21

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?"

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
Bob Horn
  • 33,387
  • 34
  • 113
  • 219

5 Answers5

33

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.

Community
  • 1
  • 1
Martin Liversage
  • 104,481
  • 22
  • 209
  • 256
  • But doesn't the view model just have a binding to IsSelected? How does it actually get the value? – Bob Horn Feb 04 '12 at 18:19
  • 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 Feb 04 '12 at 18:28
  • 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. – Bob Horn Feb 04 '12 at 18:32
  • @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. – Martin Liversage Feb 04 '12 at 18:33
  • So do each one of my entities (items in the treeview) need to have an IsSelected property? – Bob Horn Feb 04 '12 at 18:36
  • @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)`. – Martin Liversage Feb 04 '12 at 18:36
  • 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... – Bob Horn Feb 04 '12 at 18:40
  • Ahhhh... your last edit looks promising: public ChildViewModel SelectedItem... Let me give that a shot... – Bob Horn Feb 04 '12 at 18:46
  • 3
    @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. – Martin Liversage Feb 04 '12 at 18:46
  • So what, in the view, gets bound to SelectedItem in the view model? (Using your public ChildViewModel SelectedItem idea.) – Bob Horn Feb 04 '12 at 19:10
  • 1
    @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. – Martin Liversage Feb 04 '12 at 19:20
  • 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. – Bob Horn Feb 04 '12 at 19:22
  • 1
    @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. – Martin Liversage Feb 04 '12 at 19:33
  • 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 Feb 04 '12 at 19:41
  • @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 Mar 04 '13 at 18:27
  • @BobHorn p.s. the link provided by redfoxlee below is an awesome resource. – Rachael Mar 04 '13 at 18:28
  • @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;` – Bob Horn Mar 04 '13 at 23:18
  • @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 Mar 06 '13 at 22:28
  • @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. :( – Rachael Mar 07 '13 at 00:37
  • @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. – Bob Horn Mar 07 '13 at 01:48
  • 1
    @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. – Martin Liversage Mar 07 '13 at 08:21
6

I would probably use the SelectedItemChanged event to set a respective property on your VM.

H.B.
  • 166,899
  • 29
  • 327
  • 400
  • 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 Feb 04 '12 at 18:20
  • 3
    @BobHorn: Not necessarily, but people are too obsessed about code behind anyway, it's not such a big deal... – H.B. Feb 04 '12 at 18:43
  • 1
    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 Feb 04 '12 at 18:48
  • 1
    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).SelectedApplicationServer = e.NewValue as ApplicationServer; – Bob Horn Feb 04 '12 at 19:55
  • @H.B. Do you mind extending you answer [here](http://stackoverflow.com/questions/7153813/wpf-mvvm-treeview-selecteditem) 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 Mar 07 '13 at 00:28
  • 1
    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! – AshbyEngineer Jun 09 '16 at 16:05
  • 1
    @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). – H.B. Jun 09 '16 at 16:20
  • 1
    @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! :) – AshbyEngineer Jun 13 '16 at 16:42
  • 1
    @AshbyEngineer: I usually call that "cargo cult programming", happens quite a lot :) (Oh, that even has [its own wikipedia article](https://en.wikipedia.org/wiki/Cargo_cult_programming)!) – H.B. Jun 14 '16 at 08:17
1

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.

daspn
  • 51
  • 6
0

Somewhat late to the party but for those who are coming across this now, my solution was:

  1. Add a reference to 'System.Windows.Interactivity'
  2. Add the following code into your treeview element. <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.

melodiouscode
  • 2,105
  • 1
  • 20
  • 41
0

Also late to the party but as an alternative for MVVMLight users:

  1. Bind the TreeViewItem to your ViewModel to get changes of the IsSelected property.
  2. Create a MVVMLight Message (like PropertyChangeMessage) sending the SelectedItem ViewModel or Model item
  3. Register your TreeView host (or some other ViemModels if necessary) to listen for this message.

The whole implementation is very fast and works fine.

Here the IsSelected Property (SourceItem is the Model part of the selected ViewModel item):

       Public Property IsSelected As Boolean
        Get
            Return _isSelected
        End Get
        Set(value As Boolean)
            If Me.HasImages Then
                _isSelected = value
                OnPropertyChanged("IsSelected")
                Messenger.Default.Send(Of SelectedImageFolderChangedMessage)(New SelectedImageFolderChangedMessage(Me, SourceItem, "SelectedImageFolder"))
            Else
                Me.IsExpanded = Not Me.IsExpanded
            End If
        End Set
    End Property

and here the VM host code:

    Messenger.Default.Register(Of SelectedImageFolderChangedMessage)(Me, AddressOf NewSelectedImageFolder)

    Private Sub NewSelectedImageFolder(msg As SelectedImageFolderChangedMessage)
        If msg.PropertyName = "SelectedImageFolder" Then
            Me.SelectedFolderItem = msg.NewValue
        End If
    End Sub
Mr.CQ
  • 1
  • 5