Get Selected TreeViewItem Using MVVM
Solution 1
To do what you want you can modify the ItemContainerStyle
of the TreeView
:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
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:
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
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 set IsSelected
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 the TreeView
and that has a collection of child view-models) you can implement it like this:
public ChildViewModel SelectedItem {
get { return Items.FirstOrDefault(i => i.IsSelected); }
}
If you don't want to track selection on each individual item on the tree you can still use the SelectedItem
property on the TreeView
. 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:
<TreeView x:Name="treeView">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction
Command="{Binding SetSelectedItemCommand}"
CommandParameter="{Binding SelectedItem, ElementName=treeView}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
You will have to add a property SetSelectedItemCommand
on the DataContext
of the TreeView
returning an ICommand
. When the selected item of the tree view changes the Execute
method on the command is called with the selected item as the parameter. The easiest way to create a command is probably to use a DelegateCommand
(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.
Solution 2
I would probably use the SelectedItemChanged
event to set a respective property on your VM.
Solution 3
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.
Bob Horn
I'm a software developer that is passionate about elegant, quality code.
Updated on July 09, 2022Comments
-
Bob Horn almost 2 years
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?"
-
Bob Horn over 12 yearsBut doesn't the view model just have a binding to IsSelected? How does it actually get the value?
-
Bob Horn over 12 yearsWouldn't I need to use code-behind to handle the event? I'm trying to be pure about this and I have no code in the code-behind files so far.
-
Bob Horn over 12 yearsAnd by value, I mean the value of the selected item. I'm not looking just to know if something is selected, I want to know the value of the selected item.
-
Bob Horn over 12 yearsSo, I just noticed that you wrote this: "(the view-model for each item in the tree)." I don't have a view model for each item in the tree. Each item in the tree is an item in one list in the one view model.
-
Martin Liversage over 12 years@BobHorn: Each
TreeViewItem
generated by theTreeView
has anIsSelected
property. Each item in tree has a corresponding view-model. My answer describes how you can bind the property on the tree view items in the UI to a boolean property in your view model. The value is exchanged using databinding. -
Bob Horn over 12 yearsSo do each one of my entities (items in the treeview) need to have an IsSelected property?
-
Martin Liversage over 12 years@BobHorn: You need to have a view-model for each item in the tree. Otherwise, working with the selected item of a tree view will become very painful. In you parent view-model is is very easy to expose a
SelectedItem
property by returningItems.FirstOrDefault(i => i.IsSelected)
. -
Bob Horn over 12 yearsI must be missing something. I apologize, but I don't get it. If I have a list of Foo items in the view model, and each Foo has a Bar, and I group by that Bar, my items are showing correctly in the treeview. Now, when an item is selected, are you saying that I need to have an IsSelected property on the Foo class? And I would iterate the Foo items to see which has IsSelected to true? If so, then I need to add a property to my entity just so it can be used in a treeview...
-
H.B. over 12 years@BobHorn: Not necessarily, but people are too obsessed about code behind anyway, it's not such a big deal...
-
Bob Horn over 12 yearsAhhhh... your last edit looks promising: public ChildViewModel SelectedItem... Let me give that a shot...
-
Martin Liversage over 12 years@BobHorn: In MVVM you either wrap model objects in view-model objects or make you model objects so rich that they can function as view-models. If you wrap your
Foo
objects in aFooViewModel
and add anIsSelected
property to this view-model you will discover that the selection is easy to handle. TheTreeView
control exposes selection throughTreeViewItem
objects and not theTreeView
control iself and you need to mirror that in your view-models. -
Bob Horn over 12 yearsYeah, you're probably right, but I'll be damned if I'm going to let this treeview garbage be the breaking point... lol.
-
Bob Horn over 12 yearsSo what, in the view, gets bound to SelectedItem in the view model? (Using your public ChildViewModel SelectedItem idea.)
-
Martin Liversage over 12 years@BobHorn: Nothing gets bound to that but your question is about how to get the selected item of a tree view using MVVM and databinding. I assume you have some action in you main view-model that requires a selected item to work on.
-
Bob Horn over 12 yearsOkay, I just got it working where each Foo gets IsSelected set to true when it's selected in the treeview. However, I need some notification to my view model that a new Foo was selected. That's why I asked what, in the view, binds to something in the view model to let it know that something new was selected.
-
Martin Liversage over 12 years@BobHorn: Either call into your parent view-model directly from the setter of the
IsSelected
property in the child view-model or let the parent view-model subscribe toIPropertyChanged
notifications from the child view-model. Or you could use a more loosely coupled event aggregator design. -
Bob Horn over 12 yearsEach Foo doesn't know about the parent view model, so I'll have my view model subscribe to the IsSelected changed event of each Foo. While not simple, it seems to be the best answer. I'll accept this answer, and then maybe post another answer with my final code, so others can see how this all came together. The bottom line is that I need a FooWrapper, so I can treat each Foo as its own view model. Thank you so much for your patience and help.
-
Bob Horn over 12 yearsYou know, after going through all of that conversation above, I really may end up doing this. It's way simpler. All I had to do was add this to the treeview control: SelectedItemChanged="TreeView_SelectedItemChanged". Then in the code behind, for that method, all I needed was one line of code: ((ApplicationServerViewModel)DataContext).SelectedApplicationServer = e.NewValue as ApplicationServer;
-
Rachael about 11 years@BobHorn: Did you ever get this working after wrapping each object with its own viewmodel? I am in the same boat and would really appreciate seeing this done and have been unable to see anyone with an example of how to make the transition. Thank you for revisiting!
-
Rachael about 11 years@BobHorn p.s. the link provided by redfoxlee below is an awesome resource.
-
Bob Horn about 11 years@UB3571 I just looked at the code again, and it appears that I cheated. I have this in the treeview control:
SelectedItemChanged="TreeView_SelectedItemChanged"
. In the xaml.cs, I set the selected item:((ApplicationServerViewModel)DataContext).SelectedApplicationServer = e.NewValue as ApplicationServer;
-
Rachael about 11 years@MartinLiversage : Would you please tell us how to raise a PropertyNotification for the ChildViewModel property on the ParentViewModel our views are bound to ? I have read dozens of these questions and it seems everyone needs a boost in understanding that gap. I.e. Do I need to hook up the SelectedItemChanged event that raises notification of this propertychanged? Thanks so much for revisiting. (Need further clarification after
public ChildViewModel{}..etc.
). Please note I am working in MVVM and do not want to include Blend interaction triggers. -
Rachael about 11 years@H.B. Do you mind extending you answer here to include how to actually relay the childViewModel info bound to the treeView's ItemsSource to the ParentViewModel using PropertyChanged notifications? Thanks for revisiting this :)
-
Rachael about 11 years@BobHorn SIGH I caved and used your code behind. I'm so sad for now. I've never had to compromise my mvvm structure until now. In the name of MVVM I hope someone solves this. :(
-
Bob Horn about 11 years@UB3571 I used to try and be a purist about this, but it ends up costing too much of my time. I think I now have a better balance of when to spend days on a problem, and when to cheat a little. The code still passes the most important test: simple, clean, and elegant. That's good enough for me.
-
Martin Liversage about 11 years@UB3571: I'm afraid I don't understand the details of your question. It is probably better if you ask it as a real question on Stack Overflow. If you are looking for a way to allow view models to communicate you can use an event aggregator. I don't see any value in avoiding Blend behaviors as this allows you to modify behavior of the view without loosing tool support from introducing code behind.
-
AshbyEngineer almost 8 yearsI just a discussion about this scenario with a person at work... Letting the code-behind handle the event and then executing the desired VM method by using the datacontext property is totally fine. I've seen insane work arounds to this and all they do is exactly duplicate what the event handler in the Code-behind already does. Remember, the code-behind is a partial class of the XAML! It's at an equal level! if you can do a binding or use some attached property to do this in xaml, handling the event in code-behind is just as valid!
-
H.B. almost 8 years@AshbyEngineer: Sure, the view may directly access the VM, just not the other way around (if you want strict decoupling as intended by the pattern).
-
AshbyEngineer almost 8 years@H.B. Right no problem with that... I am just shocked sometimes the lengths that are taken to solve problems that don't exist... Developers need to know why something is being done.. not just do it blindly because they think it follows some "pattern". Let's save blind faith for religion! :)
-
H.B. almost 8 years@AshbyEngineer: I usually call that "cargo cult programming", happens quite a lot :) (Oh, that even has its own wikipedia article!)