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>
"Why am I using ICollectionView? To share same collection between TreeView and DataGrid. Also for purpose of sorting, grouping and filtering."
ICollectionView
is 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 same ICollectionView
). In this case you would have to create an ICollectionView
for 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 ICollectionView
as a binding source. This means binding to the collection as usual and using its default ICollectionView
to e.g. filter/sort/group its conatined items is sufficient. You don't have to bind to the ICollectionView
explicitly to see the results.
"Why am I virtualizing data? There is about 155,000 MyModel objects in my collection so it's quite laggy."
DataGrid
is virtualizing rows by default (your configuration is redundant). TreView
is not. You would have to modify the TreeView
template 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.VerticalScrollBarVisibility
to Visible
. Setting it to Auto
causes the ScrollViewer
to measure its layout continuously to check whether the scroll bars have to be rendered or not.
A simple MVVM solution is to handle the TreeView.SelectedItemChanged
event and send the value of the read-only TreeView.SelectedItem
property to the 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;
}
}
Alternatively, you can add a IsSelected
property to the item model. Providing a related Selected
and Unselected
event 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
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>