I am totally new to whole WPF world. I want to create a simple (for now) app with only one window but in MVVM way.
I'm build it in .NET 7 with nuget packages: CommunityToolkit.Mvvm 8.1.0 and Microsoft.Xaml.Behaviors.Wpf 1.1.39.
I'm looking for a way to bind SelectedItem property in TreeViewMyModelList.
I want to retrieve MyModel object to show more detailed information on right panel (green).
The problem about SelectedItem is that it has only getter, no setter.
Also I wanted to handle SelectedItemChanged event, but since I want to do it in MVVM way I don't want to mess with my MainWindow.xaml.cs, the only things are there InitializeComponent(); and DataContext = new MainWindowViewModel();. Decided to add Microsoft.Xaml.Behaviors.Wpf but I don't even know how to use it.
Another thing that I want to achieve is to show on second DataGrid (lower, blue) a list of parent node selected item.
For example: if I click on Some_Name2 item then show list of MyModel objects grouped by GroupName Some_Group_01.
If I click on Some_Group6 item then show list of MyModel objects grouped by ShortName short_03 then by every group in it.
Upper DataGrid will be used to show data depends on FilterValue.
Why am I using ICollectionView? To share same collection between TreeView and DataGrid. Also for purpose of sorting, grouping and filtering.
Why am I virtualizing data? There is about 155,000 MyModel objects in my collection so it's quite laggy.
I would appreciate every tip and trick :)
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>
ICollectionViewis not relevant in terms of collection i.e. instance sharing unless you want to allow multiple data views to implement independent sorting/filtering/grouping (which you are not doing - both data views bind to the sameICollectionView). In this case you would have to create anICollectionViewfor each data view explicitly (the default view can't be used).Note, when binding to a collection, the binding engine will always implicitly use the collection's default
ICollectionViewas a binding source. This means binding to the collection as usual and using its defaultICollectionViewto e.g. filter/sort/group its conatined items is sufficient. You don't have to bind to theICollectionViewexplicitly to see the results.DataGridis virtualizing rows by default (your configuration is redundant).TreViewis not. You would have to modify theTreeViewtemplate to enable UI virtualization.Displaying 155k items is not wise. Aside from UI virtualization you should consider data virtualization too. A user will never view 155k items. Maybe he's interested in 10 items.
You can let the user apply a filter before you load any items. If this still results in too many items, you can consider to fetch items/tree levels dynamically in addition. For example, you preload the first three levels of the pre-filtered tree. Then when the user expands a level you read and add a new level.
If you are concerned about performance and expect to display enough items to require scrolling, you must set
ScrollViewer.VerticalScrollBarVisibilitytoVisible. Setting it toAutocauses theScrollViewerto measure its layout continuously to check whether the scroll bars have to be rendered or not.Solution 1
A simple MVVM solution is to handle the
TreeView.SelectedItemChangedevent and send the value of the read-onlyTreeView.SelectedItemproperty to theDataContext:MainWindow.xaml
MainWindow.xaml.cs
Solution 2
Alternatively, you can add a
IsSelectedproperty to the item model. Providing a relatedSelectedandUnselectedevent allows to monitor selection changes. This solution requires explicit lifetime management of the data items because of the attached event listeners. This becomes very important if the source collection is dynamic (items are added/removed frequently).TreeViewItemModel
MainViewModel.cs
MainWindow.xaml