Skip to content
Open
Next Next commit
added SelectedItems DP and got it to a working state, even with keepi…
…ng state when collapsing items. Tests are missing
  • Loading branch information
corvinsz committed May 2, 2025
commit 2a6b472cb397e4faba7a18aa958c54935faec26e
2 changes: 1 addition & 1 deletion src/MainDemo.Wpf/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
If you would prefer to use your own colors, there is an option for that as well:
PrimaryColor and SecondaryColor also support the constant string "Inherit" to specify the color should use the system theme accent color
-->
<materialDesign:CustomColorTheme BaseTheme="Light" PrimaryColor="Aqua" SecondaryColor="#FF006400" />
<!--<materialDesign:CustomColorTheme BaseTheme="Light" PrimaryColor="Aqua" SecondaryColor="#FF006400" />-->

<!-- You can also use the built-in theme dictionaries: -->
<!--
Expand Down
20 changes: 16 additions & 4 deletions src/MainDemo.Wpf/Domain/TreesViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ public sealed class TreesViewModel : ViewModelBase

public AnotherCommandImplementation RemoveListTreeItemCommand { get; }

public AnotherCommandImplementation RemoveSelectedListTreeItemCommand { get; }

public ObservableCollection<TestItem?> SelectedTreeItems { get; } = [];

public TestItem? SelectedTreeItem
{
get => _selectedTreeItem;
Expand All @@ -119,7 +123,7 @@ public object? SelectedItem
public TreesViewModel()
{
Random random = new();
for(int i = 0; i < 10; i++)
for (int i = 0; i < 10; i++)
{
TreeItems.Add(CreateTestItem(random, 1));
}
Expand All @@ -128,8 +132,8 @@ static TestItem CreateTestItem(Random random, int depth)
{
int numberOfChildren = depth < 5 ? random.Next(0, 6) : 0;
var children = Enumerable.Range(0, numberOfChildren).Select(_ => CreateTestItem(random, depth + 1));
var rv = new TestItem(GenerateString(random.Next(4, 10)), children);
foreach(var child in rv.Items)
var rv = new TestItem(GenerateString(random.Next(4, 10)), children);
foreach (var child in rv.Items)
{
child.Parent = rv;
}
Expand All @@ -154,7 +158,7 @@ static TestItem CreateTestItem(Random random, int depth)
{
if (items is IEnumerable enumerable)
{
foreach(TestItem testItem in enumerable)
foreach (TestItem testItem in enumerable)
{
if (testItem.Parent is { } parent)
{
Expand Down Expand Up @@ -226,6 +230,14 @@ static TestItem CreateTestItem(Random random, int depth)
}
},
_ => SelectedItem != null);

RemoveSelectedListTreeItemCommand = new(item =>
{
if (item is TestItem treeItem)
{
SelectedTreeItems.Remove(treeItem);
}
});
}

private static string GenerateString(int length)
Expand Down
122 changes: 77 additions & 45 deletions src/MainDemo.Wpf/Trees.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -209,53 +209,85 @@
Grid.Column="2"
VerticalContentAlignment="Top"
UniqueKey="trees_3">
<Grid>
<materialDesign:TreeListView MinWidth="220" MaxHeight="450"
ItemsSource="{Binding TreeItems}"
SelectedItem="{Binding SelectedTreeItem}">
<materialDesign:TreeListView.Resources>
<HierarchicalDataTemplate DataType="{x:Type domain:TestItem}"
ItemsSource="{Binding Items, Mode=OneTime}">
<TextBlock Margin="3,2" Text="{Binding Name, Mode=OneTime}" />
</HierarchicalDataTemplate>
<StackPanel Orientation="Horizontal">
<Grid>
<materialDesign:TreeListView MinWidth="220"
MaxHeight="450"
ItemsSource="{Binding TreeItems}"
SelectedItem="{Binding SelectedTreeItem}"
SelectedItems="{Binding SelectedTreeItems}">
<materialDesign:TreeListView.Resources>
<HierarchicalDataTemplate DataType="{x:Type domain:TestItem}"
ItemsSource="{Binding Items, Mode=OneTime}">
<TextBlock Margin="3,2" Text="{Binding Name, Mode=OneTime}" />
</HierarchicalDataTemplate>

<HierarchicalDataTemplate DataType="{x:Type domain:MovieCategory}"
ItemsSource="{Binding Movies, Mode=OneTime}">
<TextBlock Margin="3,2" Text="{Binding Name, Mode=OneTime}" />
</HierarchicalDataTemplate>

<DataTemplate DataType="{x:Type domain:Movie}">
<TextBlock Margin="3,2"
Text="{Binding Name, Mode=OneTime}"
ToolTip="{Binding Director, Mode=OneTime}" />
</DataTemplate>
</materialDesign:TreeListView.Resources>

<!--
Because Data Virtualization is enabled on this tree view by default, if you don't bind the IsExpanded property to something in the bound view model,
you can loose the expanded state of items when the TreeListViewItem is recycled.

For more information:
https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/issues/3640#issuecomment-2274086113
https://learn.microsoft.com/dotnet/desktop/wpf/advanced/optimizing-performance-controls?view=netframeworkdesktop-4.8&WT.mc_id=DT-MVP-5003472
-->
<materialDesign:TreeListView.ItemContainerStyle>
<Style TargetType="materialDesign:TreeListViewItem" BasedOn="{StaticResource {x:Type materialDesign:TreeListViewItem}}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
</Style>
</materialDesign:TreeListView.ItemContainerStyle>
</materialDesign:TreeListView>
<StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Right">
<Button Command="{Binding AddListTreeItemCommand}"
ToolTip="Add an item"
Content="{materialDesign:PackIcon Kind=Add}"/>

<Button Command="{Binding RemoveListTreeItemCommand}"
ToolTip="Remove selected item(s)"
Content="{materialDesign:PackIcon Kind=Remove}"/>

<HierarchicalDataTemplate DataType="{x:Type domain:MovieCategory}"
ItemsSource="{Binding Movies, Mode=OneTime}">
<TextBlock Margin="3,2" Text="{Binding Name, Mode=OneTime}" />
</HierarchicalDataTemplate>
</StackPanel>
</Grid>

<DataTemplate DataType="{x:Type domain:Movie}">
<TextBlock Margin="3,2"
Text="{Binding Name, Mode=OneTime}"
ToolTip="{Binding Director, Mode=OneTime}" />
</DataTemplate>
</materialDesign:TreeListView.Resources>

<!--
Because Data Virtualization is enabled on this tree view by default, if you don't bind the IsExpanded property to something in the bound view model,
you can loose the expanded state of items when the TreeListViewItem is recycled.

For more information:
https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/issues/3640#issuecomment-2274086113
https://learn.microsoft.com/dotnet/desktop/wpf/advanced/optimizing-performance-controls?view=netframeworkdesktop-4.8&WT.mc_id=DT-MVP-5003472
-->
<materialDesign:TreeListView.ItemContainerStyle>
<Style TargetType="materialDesign:TreeListViewItem" BasedOn="{StaticResource {x:Type materialDesign:TreeListViewItem}}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
</Style>
</materialDesign:TreeListView.ItemContainerStyle>
</materialDesign:TreeListView>
<StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Right">
<Button Command="{Binding AddListTreeItemCommand}"
ToolTip="Add an item"
Content="{materialDesign:PackIcon Kind=Add}"/>

<Button Command="{Binding RemoveListTreeItemCommand}"
ToolTip="Remove selected item(s)"
Content="{materialDesign:PackIcon Kind=Remove}"/>

</StackPanel>
</Grid>
<GroupBox Margin="4,0,0,0" Header="TreeListView.SelectedItems">
<ListBox ItemsSource="{Binding SelectedTreeItems}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding Name}"
VerticalAlignment="Center" />
<Button Grid.Column="1"
Padding="4"
Width="{Binding Path=ActualHeight, RelativeSource={RelativeSource Mode=Self}}"
Command="{Binding DataContext.RemoveSelectedListTreeItemCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding .}"
Content="{materialDesign:PackIcon Kind=Bin}"
Foreground="Red"
Style="{StaticResource MaterialDesignFlatButton}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</GroupBox>

</StackPanel>

</smtx:XamlDisplay>

<TextBlock Grid.Row="2"
Expand Down
120 changes: 119 additions & 1 deletion src/MaterialDesignThemes.Wpf/TreeListView.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using System.Collections;
using System.Collections.Specialized;
using System.Windows.Automation.Peers;
using MaterialDesignThemes.Wpf.Automation.Peers;
using MaterialDesignThemes.Wpf.Internal;

namespace MaterialDesignThemes.Wpf;

//TODO: Implement bindable property for getting selected items
//TODO: Implement GridView support for having columns
public class TreeListView : ListView
{
Expand All @@ -20,6 +20,41 @@ public double LevelIndentSize

internal TreeListViewItemsCollection? InternalItemsSource { get; set; }

internal bool IsCollapsingItem { get; set; } = false;
public new IList SelectedItems
{
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
public static new readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register(
nameof(SelectedItems),
typeof(IList),
typeof(TreeListView),
new FrameworkPropertyMetadata(default, OnSelectedItemsPropertyChanged));

private static void OnSelectedItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TreeListView tree)
{
// Unsubscribe old collection changed
if (e.OldValue is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= tree.SelectedItems_CollectionChanged;
}

// Subscribe new collection changed
if (e.NewValue is INotifyCollectionChanged newCollection)
{
newCollection.CollectionChanged += tree.SelectedItems_CollectionChanged;
}

tree.SyncUISelectionWithSelectedItems();
}
}



static TreeListView()
{
ItemsSourceProperty.OverrideMetadata(typeof(TreeListView), new FrameworkPropertyMetadata()
Expand All @@ -30,6 +65,85 @@ static TreeListView()

public TreeListView()
{
SelectionChanged += TreeListView_SelectionChanged;
}

private void TreeListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Keep the DP collection in sync when UI selection changes
if (SelectedItems is null)
{
return;
}

// Remove unselected
foreach (var item in e.RemovedItems)
{
if (!IsCollapsingItem)
{
SelectedItems.Remove(item);
}
}

// Add newly selected
foreach (var item in e.AddedItems)
{
if (!SelectedItems.Contains(item))
{
SelectedItems.Add(item);
}
}
}

private void SelectedItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// NB: Everytime we modify the base.SelectedItems we have to unsubscribe from the SelectionChanged event
// The base.SelectedItems only contains the visually selected items,
// while the SelectedItems contains all selected items, even ones which are not "visible"
this.SelectionChanged -= TreeListView_SelectionChanged;

if (e.Action == NotifyCollectionChangedAction.Reset)
{
base.SelectedItems.Clear();
foreach (var item in SelectedItems)
{
base.SelectedItems.Add(item);
}
}
else
{
foreach (var item in e.OldItems ?? Array.Empty<object>())
{
if (!IsCollapsingItem)
{
base.SelectedItems.Remove(item);
}
}

foreach (var item in e.NewItems ?? Array.Empty<object>())
{
if (!base.SelectedItems.Contains(item))
{
base.SelectedItems.Add(item);
}
}
}

this.SelectionChanged += TreeListView_SelectionChanged;
}

/// <summary>
/// Updates/syncs the List containing all SelectedItems with the visually selected items (base.SelectedItems)
/// </summary>
private void SyncUISelectionWithSelectedItems()
{
this.SelectionChanged -= TreeListView_SelectionChanged;
base.SelectedItems.Clear();
foreach (var item in SelectedItems ?? Array.Empty<object>())
{
base.SelectedItems.Add(item);
}
this.SelectionChanged += TreeListView_SelectionChanged;
}

protected override AutomationPeer OnCreateAutomationPeer()
Expand Down Expand Up @@ -96,10 +210,14 @@ internal void ItemExpandedChanged(TreeListViewItem item)
{
itemsSource.InsertWithLevel(i + index + 1, children[i], parentLevel + 1);
}
// if a node, which has a selected childs, is expanded (added) we need to visually select the child (i.e.: update base.SelectedItems)
SyncUISelectionWithSelectedItems();
}
else
{
IsCollapsingItem = true;
itemsSource.RemoveChildrenOfOffsetAdjustedItem(index);
IsCollapsingItem = false;
}
}
}
Expand Down