Treeview, ICollectionView和绑定到SelectedValue或SelectedItemChang



我对整个WPF世界完全陌生。我想创建一个简单的(目前)应用程序只有一个窗口,但在MVVM的方式。

我在中构建它。净7CommunityToolkit.Mvvm 8.1.0Microsoft.Xaml.Behaviors.Wpf 1.1.39.

我的应用程序布局

我正在寻找一种方法来绑定SelectedItem属性在TreeViewMyModelList

我想检索MyModel对象以在右面板(绿色)上显示更详细的信息。

SelectedItem的问题是它只有getter,没有setter。

我也想处理SelectedItemChanged事件,但因为我想在MVVM的方式我不想搞砸我的MainWindow.xaml.cs,唯一的事情是有InitializeComponent();DataContext = new MainWindowViewModel();。决定添加Microsoft.Xaml.Behaviors.Wpf,但我甚至不知道如何使用它。

我想要实现的另一件事是在第二个DataGrid(下方,蓝色)上显示父节点选中项的列表。

例如:如果我点击Some_Name2项目,然后显示MyModel对象按GroupNameSome_Group_01分组的列表.

如果我点击Some_Group6项目,然后显示ShortNameshort_03分组的MyModel对象列表然后是其中的每一组

上部DataGrid将用于显示依赖于FilterValue的数据。

为什么我使用ICollectionView?在TreeViewDataGrid之间共享同一个集合。也可用于排序、分组和过滤。

为什么要虚拟化数据?在我的收藏中大约有155,000个MyModel对象,所以它相当滞后。

我将感谢你的每一个提示和技巧:)

MyModel.cs

public sealed class MyModel
{
public string Name { get; set; }
public string Text { get; set; }
public string GroupName { get; set; }
public string ShortName { get; set; }
public int Number { get; set; }
public string Description { get; set; }
public string EvenMoreInfo { get; set; }
}

MainWindowViewModel.cs

public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private string _filterValue = string.Empty;
private readonly List<LoadedDataFromFile> _loadedDataFromFile;
private List<MyModel> myModelList;
public ICollectionView TreeViewCollection { get; }
public MainWindowViewModel()
{
// loading data from file and transforming it into `MyModel` list
// for this example i populate it here
myModelList = new List<MyModel>()
{
new MyModel {Name="Some_Name1", GroupName="Some_Group_01", ShortName="short_01", Text="some_text", Number=1},
new MyModel {Name="Some_Name2", GroupName="Some_Group_01", ShortName="short_01", Text="some_text", Number=2},
new MyModel {Name="Some_Name3", GroupName="Some_Group_02", ShortName="short_01", Text="some_text", Number=3},
new MyModel {Name="Some_Name4", GroupName="Some_Group_02", ShortName="short_01", Text="some_text", Number=4},
new MyModel {Name="Some_Name5", GroupName="Some_Group_01", ShortName="short_02", Text="some_text", Number=5},
new MyModel {Name="Some_Name6", GroupName="Some_Group_01", ShortName="short_02", Text="some_text", Number=6},
new MyModel {Name="Some_Name7", GroupName="Some_Group_01", ShortName="short_02", Text="some_text", Number=7},
new MyModel {Name="Some_Name8", GroupName="Some_Group_02", ShortName="short_02", Text="some_text", Number=8},
new MyModel {Name="Some_Name9", GroupName="Some_Group_05", ShortName="short_03", Text="some_text", Number=9},
new MyModel {Name="Some_Name10", GroupName="Some_Group_05", ShortName="short_03", Text="some_text", Number=10},
new MyModel {Name="Some_Name11", GroupName="Some_Group_06", ShortName="short_03", Text="some_text", Number=11},
new MyModel {Name="Some_Name12", GroupName="Some_Group_07", ShortName="short_03", Text="some_text", Number=12},
};

TreeViewCollection = CollectionViewSource.GetDefaultView(myModelList);
TreeViewCollection.Filter = FilterCollection;
var pgd = new PropertyGroupDescription(nameof(MyModel.ShortName));
pgd.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
TreeViewCollection.GroupDescriptions.Add(pgd);
pgd = new PropertyGroupDescription(nameof(MyModel.GroupName));
pgd.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
TreeViewCollection.GroupDescriptions.Add(pgd);
TreeViewCollection.SortDescriptions.Add(new SortDescription(nameof(MyModel.Number), ListSortDirection.Ascending));
}
private bool FilterCollection(object obj)
{
if (obj is not MyModel model)
{
return false;
}
return model.Name!.ToLower().Contains(FilterValue.ToLower());
}
partial void OnFilterValueChanged(string value)
{
TreeViewCollection.Refresh();
}
}

MainWindow.xaml

<Window x:Class="WPFUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors"
xmlns:viewmodels="clr-namespace:WPFUI.ViewModels" d:DataContext="{d:DesignInstance Type=viewmodels:MainWindowViewModel}"

mc:Ignorable="d"
FontSize="16"
Title="Title" Height="450" Width="800"
>
<Grid Background="Black">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="4*" />
<RowDefinition Height="4*" />
<RowDefinition Height="*" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Grid.ColumnSpan="3">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_Open" />
</MenuItem>
</Menu>
</DockPanel>
</Grid>
<Grid Grid.Row="1">
<TextBox Text="{Binding FilterValue, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
<Grid Grid.Row="2" Grid.Column="1" Background="DarkGreen">
<DataGrid x:Name="Grid_Upper" Grid.Row="2"
ItemsSource="{Binding TreeViewCollection}"
AlternatingRowBackground="GreenYellow"
HeadersVisibility="Column"  AutoGenerateColumns="False"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="True"
CanUserResizeColumns="True" CanUserResizeRows="True" CanUserSortColumns="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.IsVirtualizingWhenGrouping="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" />
<DataGridTemplateColumn Header="Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" TextWrapping="Wrap" Padding="10,10,10,10" MinWidth="100" Width="300" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Label Content="{Binding Name}" FontWeight="Bold"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</DataGrid.GroupStyle>
</DataGrid>
</Grid>

<Grid Grid.Row="2" Grid.RowSpan="2">
<TreeView x:Name="TreeViewMyModelList" ItemsSource="{Binding TreeViewCollection.Groups}"                      
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
>
<Behaviors:Interaction.Triggers>
<Behaviors:EventTrigger EventName="SelectedItemChanged">
</Behaviors:EventTrigger>
</Behaviors:Interaction.Triggers>

<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Items}">
<TextBlock VerticalAlignment="Center" Text="{Binding Path=Name}"></TextBlock>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Grid>
<Grid Grid.Row="3" Grid.Column="1" Background="LightBlue">
<TextBlock>Second Grid shows group of selected item in TreeViewMyModelList</TextBlock>
</Grid>
<Grid Grid.Row="2" Grid.Column="2" Background="green" Grid.RowSpan="2">
<TextBlock>More information about MyModel</TextBlock>
</Grid>
<Grid Grid.Row="4" Height="300">
</Grid>
<Grid Grid.Row="5" Grid.ColumnSpan="3" Background="Gray">
</Grid>
</Grid>
</Window>

"为什么我要使用ICollectionView?在TreeView和DataGrid之间共享相同的集合。也用于排序,分组和过滤!">

ICollectionView在集合方面是不相关的,即实例共享,除非你想允许多个数据视图实现独立的排序/过滤/分组(你没有这样做-两个数据视图绑定到同一个ICollectionView)。在这种情况下,您必须显式地为每个数据视图创建一个ICollectionView(不能使用默认视图)。

注意,当绑定到集合时,绑定引擎总是隐式地使用集合的默认ICollectionView作为绑定源。这意味着像往常一样绑定到集合并使用其默认的ICollectionView来过滤/排序/分组其包含的项目就足够了。您不必显式绑定到ICollectionView来查看结果。

"为什么我要虚拟化数据?我的收藏中大约有155,000个MyModel对象,所以它相当滞后。">

DataGrid默认情况下正在虚拟化行(您的配置是冗余的)。TreView不是。您必须修改TreeView模板以启用UI虚拟化。
显示155k项是不明智的。除了UI虚拟化,您还应该考虑数据虚拟化。用户永远不会查看155k个项目。也许他对10件物品感兴趣。
您可以让用户在加载任何项目之前应用过滤器。如果这仍然导致太多的项目,你可以考虑动态获取项目/树级别。例如,预加载预过滤树的前三层。然后,当用户扩展一个关卡时,你读取并添加一个新关卡。

如果您关心性能,并希望显示足够的项目需要滚动,您必须将ScrollViewer.VerticalScrollBarVisibility设置为Visible。将其设置为Auto会导致ScrollViewer持续测量其布局,以检查滚动条是否必须呈现。

解决方案1

一个简单的MVVM解决方案是处理TreeView.SelectedItemChanged事件并将只读TreeView.SelectedItem属性的值发送给DataContext:

MainWindow.xaml

<Window>
<Window.Resources>
<!-- Bind the control's SelectedTreeViewItem property to the DataContext -->
<Style TargetType="local:MainWindow">
<Setter Property="SelectedTreeViewItem"
Value="{Binding SelectedDataItem}" />
</Style>
</Window.Resources>
<TreeView SelectedItemChanged="TreeView_SelectedItemChanged" />
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{
public object SelectedTreeViewItem
{
get => (object)GetValue(SelectedTreeViewItemProperty);
set => SetValue(SelectedTreeViewItemProperty, value);
}
public static readonly DependencyProperty SelectedTreeViewItemProperty = DependencyProperty.Register(
"SelectedTreeViewItem",
typeof(object),
typeof(MainWindow),
new FrameworkPropertyMetadata(default(object), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var treeView = sender as System.Windows.Controls.TreeView;
this.SelectedTreeViewItem = treeView.SelectedItem;
}
}

解决方案2

或者,您可以向项目模型添加一个IsSelected属性。提供相关的SelectedUnselected事件可以监视选择的更改。由于附加了事件侦听器,此解决方案需要对数据项进行显式的生命周期管理。如果源集合是动态的(经常添加/删除项),这一点就变得非常重要。

TreeViewItemModel

class TreeViewItemModel : INotifyPropertyChanged
{
// TODO::Implement INotifyPropertyChanged and raise PropertyCHanged event from property setters
public event EvenetHandler Selected;
public event EvenetHandler Unselected;
// TODO::Raise Selected and Unselected event from setter
public bool IsSelected { get; set; }
}

MainViewModel.cs

class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<TreeViewItemModel> TreeViewData { get; }
public void AddTreeViewData(TreeViewItemModel dataItem)
{
this.TreeViewData.Add(dataItem);
dataItem.Selected += OnTreeViewItemSelected;
dataItem.Selected += OnTreeViewItemUnselected;
}
public void RemoveTreeViewData(TreeViewItemModel dataItem)
{
this.TreeViewData.Remove(dataItem);
dataItem.Selected -= OnTreeViewItemSelected;
dataItem.Selected -= OnTreeViewItemUnselected;
}
public void OnTreeViewItemSelected(object sender, EventArgs e)
{
}
public void OnTreeViewItemUnselected(object sender, EventArgs e)
{
}
}

MainWindow.xaml

<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<!-- Connect the item container to the item -->
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>

上面的答案解释了一切,但我决定在编辑后发布完整的代码。我删除了Microsoft.Xaml.Behaviors.Wpf 1.1.39nuget包,因为我不在这里使用它。

MainWindow.xaml.cs

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
public object SelectedTreeViewItem
{
get => (object)GetValue(SelectedTreeViewItemProperty);
set => SetValue(SelectedTreeViewItemProperty, value);
}
public static readonly DependencyProperty SelectedTreeViewItemProperty = DependencyProperty.Register(
nameof(SelectedTreeViewItem),
typeof(object),
typeof(MainWindow),
new FrameworkPropertyMetadata(default(object), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var treeView = sender as System.Windows.Controls.TreeView;
this.SelectedTreeViewItem = treeView.SelectedItem;
}    
}

MainWindow.xaml

<Window x:Class="WPFUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodels="clr-namespace:WPFUI.ViewModels" d:DataContext="{d:DesignInstance Type=viewmodels:MainWindowViewModel}"
xmlns:local="clr-namespace:WPFUI"

mc:Ignorable="d"
FontSize="16"
Title="Title" Height="450" Width="800"
>
<Window.Resources>
<Style TargetType="local:MainWindow">
<Setter Property="SelectedTreeViewItem"
Value="{Binding SelectedDataItem}" />
</Style>
</Window.Resources>
<Grid Background="Black">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="4*" />
<RowDefinition Height="4*" />
<RowDefinition Height="*" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Grid.ColumnSpan="3">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_Open" />
</MenuItem>
</Menu>
</DockPanel>
</Grid>
<Grid Grid.Row="1">
<TextBox Text="{Binding FilterValue, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
<Grid Grid.Row="2" Grid.Column="1" Background="DarkGreen">
<DataGrid x:Name="Grid_Upper" Grid.Row="2"
ItemsSource="{Binding TreeViewCollection}"
AlternatingRowBackground="GreenYellow"
HeadersVisibility="Column"  AutoGenerateColumns="False"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="True"
CanUserResizeColumns="True" CanUserResizeRows="True" CanUserSortColumns="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.IsVirtualizingWhenGrouping="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" />
<DataGridTemplateColumn Header="Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" TextWrapping="Wrap" Padding="10,10,10,10" MinWidth="100" Width="300" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Label Content="{Binding Name}" FontWeight="Bold"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</DataGrid.GroupStyle>
</DataGrid>
</Grid>

<Grid Grid.Row="2" Grid.RowSpan="2">
<TreeView x:Name="TreeViewMyModelList" ItemsSource="{Binding TreeViewCollection.Groups}"                      
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
SelectedItemChanged="TreeView_SelectedItemChanged"
ScrollViewer.VerticalScrollBarVisibility="Visible"
>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Items}">
<TextBlock VerticalAlignment="Center" Text="{Binding Path=Name}"></TextBlock>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Grid>
<Grid Grid.Row="3" Grid.Column="1" Background="LightBlue">
<TextBlock>Second Grid shows group of selected item in TreeViewMyModelList</TextBlock>
</Grid>
<Grid Grid.Row="2" Grid.Column="2" Background="green" Grid.RowSpan="2">
<!--<TextBlock>More information about MyModel</TextBlock>-->
<TextBlock Text="{Binding SelectedDataItem.Name}"></TextBlock>
</Grid>
<Grid Grid.Row="4" Height="300">
</Grid>
<Grid Grid.Row="5" Grid.ColumnSpan="3" Background="Gray">
</Grid>
</Grid>
</Window>

MainWindowViewModel.cs

public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private string _filterValue = string.Empty;
[ObservableProperty]
private object _selectedDataItem;
private readonly List<LoadedDataFromFile> _loadedDataFromFile;
private List<MyModel> myModelList;
public ICollectionView TreeViewCollection { get; }
public MainWindowViewModel()
{
// loading data from file and transforming it into `MyModel` list
// for this example i populate it here
myModelList = new List<MyModel>()
{
new MyModel {Name="Some_Name1", GroupName="Some_Group_01", ShortName="short_01", Text="some_text", Number=1},
new MyModel {Name="Some_Name2", GroupName="Some_Group_01", ShortName="short_01", Text="some_text", Number=2},
new MyModel {Name="Some_Name3", GroupName="Some_Group_02", ShortName="short_01", Text="some_text", Number=3},
new MyModel {Name="Some_Name4", GroupName="Some_Group_02", ShortName="short_01", Text="some_text", Number=4},
new MyModel {Name="Some_Name5", GroupName="Some_Group_01", ShortName="short_02", Text="some_text", Number=5},
new MyModel {Name="Some_Name6", GroupName="Some_Group_01", ShortName="short_02", Text="some_text", Number=6},
new MyModel {Name="Some_Name7", GroupName="Some_Group_01", ShortName="short_02", Text="some_text", Number=7},
new MyModel {Name="Some_Name8", GroupName="Some_Group_02", ShortName="short_02", Text="some_text", Number=8},
new MyModel {Name="Some_Name9", GroupName="Some_Group_05", ShortName="short_03", Text="some_text", Number=9},
new MyModel {Name="Some_Name10", GroupName="Some_Group_05", ShortName="short_03", Text="some_text", Number=10},
new MyModel {Name="Some_Name11", GroupName="Some_Group_06", ShortName="short_03", Text="some_text", Number=11},
new MyModel {Name="Some_Name12", GroupName="Some_Group_07", ShortName="short_03", Text="some_text", Number=12},
};

TreeViewCollection = CollectionViewSource.GetDefaultView(myModelList);
TreeViewCollection.Filter = FilterCollection;
var pgd = new PropertyGroupDescription(nameof(MyModel.ShortName));
pgd.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
TreeViewCollection.GroupDescriptions.Add(pgd);
pgd = new PropertyGroupDescription(nameof(MyModel.GroupName));
pgd.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
TreeViewCollection.GroupDescriptions.Add(pgd);
TreeViewCollection.SortDescriptions.Add(new SortDescription(nameof(MyModel.Number), ListSortDirection.Ascending));
}
private bool FilterCollection(object obj)
{
if (obj is not MyModel model)
{
return false;
}
return model.Name!.ToLower().Contains(FilterValue.ToLower());
}
partial void OnFilterValueChanged(string value)
{
TreeViewCollection.Refresh();
}
partial void OnSelectedDataItemChanged(object value)
{
// do something
}
}

最新更新