Get Selected TreeViewItem Using MVVM

28,775

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.

Share:
28,775
Bob Horn
Author by

Bob Horn

I'm a software developer that is passionate about elegant, quality code.

Updated on July 09, 2022

Comments

  • Bob Horn
    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
    Bob Horn over 12 years
    But doesn't the view model just have a binding to IsSelected? How does it actually get the value?
  • Bob Horn
    Bob Horn over 12 years
    Wouldn'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
    Bob Horn over 12 years
    And 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
    Bob Horn over 12 years
    So, 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
    Martin Liversage over 12 years
    @BobHorn: Each TreeViewItem generated by the TreeView has an IsSelected 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
    Bob Horn over 12 years
    So do each one of my entities (items in the treeview) need to have an IsSelected property?
  • Martin Liversage
    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 returning Items.FirstOrDefault(i => i.IsSelected).
  • Bob Horn
    Bob Horn over 12 years
    I 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.
    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
    Bob Horn over 12 years
    Ahhhh... your last edit looks promising: public ChildViewModel SelectedItem... Let me give that a shot...
  • Martin Liversage
    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 a FooViewModel and add an IsSelected property to this view-model you will discover that the selection is easy to handle. The TreeView control exposes selection through TreeViewItem objects and not the TreeView control iself and you need to mirror that in your view-models.
  • Bob Horn
    Bob Horn over 12 years
    Yeah, 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
    Bob Horn over 12 years
    So what, in the view, gets bound to SelectedItem in the view model? (Using your public ChildViewModel SelectedItem idea.)
  • Martin Liversage
    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
    Bob Horn over 12 years
    Okay, 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
    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 to IPropertyChanged notifications from the child view-model. Or you could use a more loosely coupled event aggregator design.
  • Bob Horn
    Bob Horn over 12 years
    Each 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
    Bob Horn over 12 years
    You 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).SelectedApplicatio‌​nServer = e.NewValue as ApplicationServer;
  • Rachael
    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
    Rachael about 11 years
    @BobHorn p.s. the link provided by redfoxlee below is an awesome resource.
  • Bob Horn
    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).SelectedApplicatio‌​nServer = e.NewValue as ApplicationServer;
  • Rachael
    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
    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
    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
    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
    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
    AshbyEngineer almost 8 years
    I 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.
    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
    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.
    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!)