So someone suggested using a WPF TreeView
, and I thought: "Yeah, that seems like the right approach." Now, hours and hours later, I simply can't believe how difficult it has been to use this control. Through a bunch of research, I was able to get the TreeView` control working, but I simply cannot find the "proper" way to get the selected item to the view model. I do not need to set the selected item from code; I just need my view model to know which item the user selected.
So far, I have this XAML, which isn't very intuitive on its own. This is all within the UserControl.Resources tag:
<CollectionViewSource x:Key="cvs" Source="{Binding ApplicationServers}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="DeploymentEnvironment"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
<!-- Our leaf nodes (server names) -->
<DataTemplate x:Key="serverTemplate">
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
<!-- Note: The Items path refers to the items in the CollectionViewSource group (our servers).
The Name path refers to the group name. -->
<HierarchicalDataTemplate x:Key="categoryTemplate"
ItemsSource="{Binding Path=Items}"
ItemTemplate="{StaticResource serverTemplate}">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
</HierarchicalDataTemplate>
And here's the treeview:
<TreeView DockPanel.Dock="Bottom" ItemsSource="{Binding Source={StaticResource cvs}, Path=Groups}"
ItemTemplate="{StaticResource categoryTemplate}">
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected}"/>
</Style>
</TreeView>
This correctly shows servers by environment (dev, QA, prod). However, I've found various ways on SO to get the selected item, and many are convoluted and difficult. Is there a simple way to get the selected item to my view model?
Note: There is a SelectedItem
property on the TreeView`, but it's read-only. What's frustrating to me is that read-only is just fine; I don't want to change it via code. But I can't use it because the compiler complains that it's read-only.
There was also a seemingly elegant suggestion to do something like this:
<ContentPresenter Content="{Binding ElementName=treeView1, Path=SelectedItem}" />
And I asked this question: "How can your a view model get this information? I get that ContentPresenter
holds the selected item, but how do we get that over to the view model?" But there is no answer yet.
So, my overall question is: "Is there a simple way to get the selected item to my view model?"
To do what you want you can modify the
ItemContainerStyle
of theTreeView
:Your view-model (the view-model for each item in the tree) then has to expose a boolean
IsSelected
property.If you want to be able to control if a particular
TreeViewItem
is expanded you can use a setter for that property too:Your view-model then has to expose a boolean
IsExpanded
property.Note that these properties work both ways so if the user selects a node in the tree the
IsSelected
property of the view-model will be set to true. On the other hand if you setIsSelected
to true on a view-model the node in the tree for that view-model will be selected. And likewise with expanded.If you don't have a view-model for each item in the tree, well, then you should get one. Not having a view-model means that you are using your model objects as view-models, but for this to work these objects require an
IsSelected
property.To expose an
SelectedItem
property on your parent view-model (the one you bind to theTreeView
and that has a collection of child view-models) you can implement it like this:If you don't want to track selection on each individual item on the tree you can still use the
SelectedItem
property on theTreeView
. However, to be able to do it "MVVM style" you need to use a Blend behavior (available as various NuGet packages - search for "blend interactivity").Here I have added an
EventTrigger
that will invoke a command each time the selected item changes in the tree:You will have to add a property
SetSelectedItemCommand
on theDataContext
of theTreeView
returning anICommand
. When the selected item of the tree view changes theExecute
method on the command is called with the selected item as the parameter. The easiest way to create a command is probably to use aDelegateCommand
(google it to get an implementation as it is not part of WPF).A perhaps better alternative that allows two-way binding without the clunky command is to use BindableSelectedItemBehavior provided by Steve Greatrex here on Stack Overflow.
I would probably use the
SelectedItemChanged
event to set a respective property on your VM.Based on Martin's answer I made a simple application showing how to apply the proposed solution.
The sample code uses the Cinch V2 framework to support MVVM but it can be easily changed to use the framework of you preference.
For those interested, here is the code on GitHub
Hope it helps.
Somewhat late to the party but for those who are coming across this now, my solution was:
<i:Interaction.Triggers> <i:EventTrigger EventName="SelectedItemChanged"> <i:InvokeCommandAction Command="{Binding SomeICommand}" CommandParameter="{Binding ElementName=treeviewName, Path=SelectedItem}" /> </i:EventTrigger> </i:Interaction.Triggers>
This will allow you to use a standard MVVM ICommand binding to access the SelectedItem without having to use code behind or some long winded work around.