Skip to content

Commit ae71f8b

Browse files
Merge pull request #100 from one-ware/feature/#99-tree-view-performance
Replace TreeView with TreeDataGrid
2 parents 1f7a11f + e0d9858 commit ae71f8b

22 files changed

Lines changed: 833 additions & 112 deletions

src/OneWare.Essentials/Models/IProjectFolder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ public interface IProjectFolder : IProjectEntry
66

77
public void Remove(IProjectEntry entry);
88

9+
public void SetIsExpandedFromView(bool newValue);
10+
911
public IProjectFile AddFile(string path, bool createNew = false);
1012

1113
public IProjectFolder AddFolder(string path, bool createNew = false);

src/OneWare.Essentials/Services/IProjectExplorerService.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace OneWare.Essentials.Services;
1010
public interface IProjectExplorerService : IDockable, INotifyPropertyChanged
1111
{
1212
public ObservableCollection<IProjectRoot> Projects { get; }
13-
public ObservableCollection<IProjectExplorerNode> SelectedItems { get; }
13+
public IReadOnlyList<IProjectExplorerNode> SelectedItems { get; }
1414
public IProjectRoot? ActiveProject { get; set; }
1515
public event EventHandler<IFile>? FileRemoved;
1616
public event EventHandler<IProjectRoot>? ProjectRemoved;
@@ -41,4 +41,7 @@ public Task<IProjectRoot?>
4141

4242
public void RegisterConstructContextMenu(
4343
Action<IReadOnlyList<IProjectExplorerNode>, IList<MenuItemViewModel>> construct);
44+
45+
public void ClearSelection();
46+
public void AddToSelection(IProjectExplorerNode node);
4447
}

src/OneWare.FolderProjectSystem/FolderProjectManager.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using OneWare.Essentials.Services;
33
using OneWare.Essentials.ViewModels;
44
using OneWare.FolderProjectSystem.Models;
5+
using OneWare.ProjectSystem.Models;
56

67
namespace OneWare.FolderProjectSystem;
78

@@ -32,17 +33,33 @@ public Task<bool> SaveProjectAsync(IProjectRoot root)
3233

3334
public static void LoadFolder(IProjectFolder folder)
3435
{
35-
var matches = Directory.EnumerateFileSystemEntries(folder.FullPath);
36-
37-
foreach (var match in matches)
36+
folder.Children.Clear();
37+
folder.Entities.Clear();
38+
39+
var options = new EnumerationOptions
40+
{
41+
AttributesToSkip = FileAttributes.Hidden | FileAttributes.System,
42+
IgnoreInaccessible = true,
43+
RecurseSubdirectories = false
44+
};
45+
var directoryMatches = Directory.EnumerateDirectories(folder.FullPath, "*", options);
46+
47+
foreach (var match in directoryMatches)
48+
{
49+
var newFolder = new ProjectFolder(Path.GetFileName(match), folder);
50+
folder.Children.Add(newFolder);
51+
folder.Entities.Add(newFolder);
52+
(folder.Root as FolderProjectRoot)!.RegisterEntry(newFolder);
53+
}
54+
55+
var fileMatches = Directory.EnumerateFiles(folder.FullPath, "*.*", options);
56+
57+
foreach (var match in fileMatches)
3858
{
39-
var relativePath = Path.GetRelativePath(folder.FullPath, match);
40-
var attributes = File.GetAttributes(match);
41-
if (attributes.HasFlag(FileAttributes.Hidden)) continue;
42-
if (attributes.HasFlag(FileAttributes.Directory))
43-
folder.AddFolder(relativePath);
44-
else
45-
folder.AddFile(relativePath);
59+
var newFile = new ProjectFile(Path.GetFileName(match), folder);
60+
folder.Children.Add(newFile);
61+
folder.Entities.Add(newFile);
62+
(folder.Root as FolderProjectRoot)!.RegisterEntry(newFile);
4663
}
4764
}
4865

src/OneWare.FolderProjectSystem/Models/FolderProjectRoot.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,7 @@ private void WatchDirectory(IProjectFolder folder)
5353
folder.Children.Add(new LoadingDummyNode());
5454
}
5555
});
56-
57-
//Console.WriteLine("watch folder: " + folder.FullPath);
58-
56+
5957
_registeredFolders.Add(folder, subscription);
6058
}
6159
catch (Exception e)

src/OneWare.FolderProjectSystem/Models/LoadingDummyNode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ public class LoadingDummyNode : IProjectExplorerNode
1717
public ObservableCollection<IImage> RightIcons { get; } = new();
1818
public bool IsExpanded { get; set; }
1919
public IBrush Background { get; set; } = Brushes.Transparent;
20-
public FontWeight FontWeight { get; set; }
20+
public FontWeight FontWeight { get; set; } = FontWeight.Regular;
2121
public float TextOpacity { get; set; } = 1f;
2222
}

src/OneWare.LibraryExplorer/OneWare.LibraryExplorer.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@
1414
<SubType>Code</SubType>
1515
</Compile>
1616
</ItemGroup>
17+
18+
<ItemGroup>
19+
<PackageReference Include="Avalonia.Controls.TreeDataGrid" Version="11.1.1" />
20+
</ItemGroup>
1721
</Project>

src/OneWare.LibraryExplorer/ViewModels/LibraryExplorerViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public void ConstructContextMenu(TopLevel topLevel)
9999
menuItems.Add(new MenuItemViewModel("Refresh")
100100
{
101101
Header = "Refresh",
102-
Command = new RelayCommand(() => PlatformHelper.OpenExplorerPath(_libraryFolderPath))
102+
Command = new AsyncRelayCommand(async() => await LoadAsync())
103103
});
104104
menuItems.Add(new MenuItemViewModel("Open Library Folder")
105105
{

src/OneWare.LibraryExplorer/Views/LibraryExplorerView.axaml

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
xmlns:behaviors="clr-namespace:OneWare.Essentials.Behaviors;assembly=OneWare.Essentials"
88
xmlns:behaviors1="clr-namespace:OneWare.ProjectExplorer.Behaviors"
99
xmlns:viewModels="clr-namespace:OneWare.LibraryExplorer.ViewModels"
10+
xmlns:views1="clr-namespace:OneWare.ProjectExplorer.Views;assembly=OneWare.ProjectExplorer"
1011
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="450"
1112
x:Class="OneWare.LibraryExplorer.Views.LibraryExplorerView"
1213
Background="{DynamicResource ThemeControlLowBrush}"
@@ -17,7 +18,6 @@
1718
BorderThickness="0">
1819

1920
<UserControl.Styles>
20-
2121
<Style Selector="TextBox">
2222
<Setter Property="Padding" Value="0" />
2323
<Setter Property="Margin" Value="0" />
@@ -26,6 +26,7 @@
2626
<Setter Property="TextWrapping" Value="NoWrap" />
2727
</Style>
2828

29+
<StyleInclude Source="avares://OneWare.ProjectExplorer/Views/ProjectExplorerTheme.axaml"></StyleInclude>
2930
</UserControl.Styles>
3031

3132
<Grid>
@@ -41,22 +42,36 @@
4142
SearchText="{Binding SearchString, Mode=TwoWay}" VerticalAlignment="Center"
4243
VerticalContentAlignment="Center" x:Name="SearchBox" Height="24" />
4344
</Border>
44-
<TreeView AutoScrollToSelectedItem="True"
45-
ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectionMode="Multiple"
46-
SelectedItems="{Binding SelectedItems, Mode=TwoWay}" Grid.Row="1" Name="ProjectTree"
47-
ItemsSource="{Binding Projects}">
48-
<TreeView.ContextMenu>
45+
<TreeDataGrid Name="ProjectTree" Grid.Row="1" CanUserSortColumns="False"
46+
ShowColumnHeaders="False" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
47+
CanUserResizeColumns="False"
48+
AutoDragDropRows="False"
49+
Source="{Binding Source}">
50+
<TreeDataGrid.ContextMenu>
4951
<ContextMenu Classes="BindMenu" x:Name="TreeViewContextMenu"
5052
ItemsSource="{Binding TreeViewContextMenu}" />
51-
</TreeView.ContextMenu>
52-
<TreeView.Styles>
53-
<Style Selector="TreeViewItem" x:DataType="models:IProjectExplorerNode">
54-
<Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
53+
</TreeDataGrid.ContextMenu>
54+
<TreeDataGrid.Styles>
55+
<Style Selector="TreeDataGridRow" x:DataType="models:IProjectExplorerNode">
5556
<Setter Property="Background" Value="{Binding Background}" />
5657
</Style>
57-
</TreeView.Styles>
58-
<TreeView.DataTemplates>
59-
<TreeDataTemplate DataType="models:IProjectExplorerNode" ItemsSource="{Binding Children}">
58+
<Style Selector="TreeDataGridRow TextBox:disabled /template/ Border#border">
59+
<Setter Property="Opacity" Value="1" />
60+
</Style>
61+
<!-- Manual two-way binding to Property IsExpanded -->
62+
<!-- If IsExpanded is bound directly, an unhandled exception when measuring the UI will occur -->
63+
<!-- Furthermore, it doesn't work to use the Func<bool> isExpanded property in the HierarchicalDataSource -->
64+
<Style Selector="TreeDataGridExpanderCell" x:DataType="models:IProjectFolder">
65+
<Setter Property="views1:TreeDataGridExtension.IsExpandedExtension"
66+
Value="{Binding IsExpanded, Mode=OneWay}" />
67+
</Style>
68+
<Style Selector="TreeDataGridExpanderCell /template/ ToggleButton#ExpanderToggleButton"
69+
x:DataType="models:IProjectFolder">
70+
<Setter Property="Command" Value="{Binding SetIsExpandedFromView}" />
71+
</Style>
72+
</TreeDataGrid.Styles>
73+
<TreeDataGrid.Resources>
74+
<DataTemplate x:Key="ProjectExplorerColumnTemplate" DataType="models:IProjectExplorerNode">
6075
<DockPanel>
6176
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
6277
<ItemsControl ItemsSource="{Binding RightIcons}">
@@ -97,15 +112,6 @@
97112
</ItemsControl>
98113
</Grid>
99114

100-
<!--
101-
<Grid DockPanel.Dock="Right" Width="11">
102-
<TextBlock HorizontalAlignment="Center"
103-
ToolTip.Tip="{Binding GitChangeStatus, Converter={StaticResource EnumToStringConverter}}"
104-
Text="{Binding GitChangeStatus, Converter={StaticResource ChangeStatusCharConverter}}"
105-
FontWeight="Bold"
106-
Foreground="{Binding GitChangeStatus, Converter={StaticResource ChangeStatusBrushConverter}}" />
107-
</Grid>-->
108-
109115
<controls:RenamingTextBox FontWeight="{Binding FontWeight}" ToolTip.Tip="{Binding Header}"
110116
VerticalAlignment="Center"
111117
IsEnabled="False" x:Name="RenamingTextBox"
@@ -114,9 +120,9 @@
114120
Opacity="{Binding TextOpacity}" />
115121
</StackPanel>
116122
</DockPanel>
117-
</TreeDataTemplate>
118-
</TreeView.DataTemplates>
119-
</TreeView>
123+
</DataTemplate>
124+
</TreeDataGrid.Resources>
125+
</TreeDataGrid>
120126
</Grid>
121127

122128

src/OneWare.ProjectExplorer/Behaviors/ProjectExplorerViewDropHandler.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ namespace OneWare.ProjectExplorer.Behaviors;
1010

1111
public class ProjectExplorerViewDropHandler : DropHandlerBase
1212
{
13-
private bool Validate<T>(TreeView treeView, DragEventArgs e, object? sourceContext, object? targetContext,
13+
private bool Validate<T>(TreeDataGrid treeView, DragEventArgs e, object? sourceContext, object? targetContext,
1414
bool bExecute) where T : IProjectExplorerNode
1515
{
1616
if (targetContext is not ProjectExplorerViewModel vm
1717
|| treeView.GetVisualAt(e.GetPosition(treeView)) is not Control { DataContext: T targetNode })
1818
return false;
19-
19+
2020
var targetParent = targetNode as IProjectFolder ?? targetNode.Parent as IProjectFolder;
2121

2222
if (targetParent == null) return false;
2323

2424
//Import files or folders from outside
25-
if (sourceContext is not ICollection<T> sourceNodes)
25+
if (sourceContext is not IReadOnlyList<T> sourceNodes)
2626
{
2727
if (e.Data.Get(DataFormats.Files) is IEnumerable<IStorageItem> files)
2828
{
@@ -41,7 +41,7 @@ private bool Validate<T>(TreeView treeView, DragEventArgs e, object? sourceConte
4141
if (!sourceNodes.All(x => x is IProjectEntry)) return false;
4242

4343
var sourceEntities = sourceNodes.Cast<IProjectEntry>().ToArray();
44-
44+
4545
foreach (var sourceNode in sourceEntities)
4646
{
4747
if (targetParent == sourceNode.Parent) return false;
@@ -52,7 +52,7 @@ private bool Validate<T>(TreeView treeView, DragEventArgs e, object? sourceConte
5252
if (sourceNode is IProjectFolder sourceFolder && targetParent.FullPath.StartsWith(sourceFolder.FullPath))
5353
return false;
5454
}
55-
55+
5656
switch (e.DragEffects)
5757
{
5858
case DragDropEffects.Copy:
@@ -81,16 +81,20 @@ private bool Validate<T>(TreeView treeView, DragEventArgs e, object? sourceConte
8181
public override bool Validate(object? sender, DragEventArgs e, object? sourceContext, object? targetContext,
8282
object? state)
8383
{
84-
if (e.Source is Control && sender is TreeView treeView)
85-
return Validate<IProjectExplorerNode>(treeView, e, sourceContext, targetContext, false);
84+
if (e.Source is Control && sender is TreeDataGrid treeView)
85+
{
86+
var status = Validate<IProjectExplorerNode>(treeView, e, sourceContext, targetContext, false);
87+
return status;
88+
}
89+
8690
return false;
8791
}
8892

8993
public override bool Execute(object? sender, DragEventArgs e, object? sourceContext, object? targetContext,
9094
object? state)
9195
{
92-
if (e.Source is Control && sender is TreeView treeView)
96+
if (e.Source is Control && sender is TreeDataGrid treeView)
9397
return Validate<IProjectExplorerNode>(treeView, e, sourceContext, targetContext, true);
94-
return false;
98+
return true;
9599
}
96100
}

0 commit comments

Comments
 (0)