Tuesday, November 30, 2010

Two-Way Binding on TreeView.SelectedItem

In this post I’m going to describe an attached-behavior for a TreeView control that allows you to achieve two-way binding on the SelectedItem property. You can grab the source code here.

[UPDATE 6 Dec 2010]: I fixed a bug that was causing it to fail to update the TreeView when the selected node was set (through binding) before the view had loaded.

Background

If you have ever tried using the TreeView control with it’s ItemsSource bound to some kind of data context then you probably know how frustrating it can be to work with the SelectedItem property. This is especially true if you are trying to follow an MVVM pattern.

Typically when you use an ItemsControl you want to create a two-way binding on two of its properties: ItemsSource and SelectedItem. But TreeView, unlike other ItemsControl subclasses, has a read-only SelectedItem property which means no two-way binding like this:

<sdk:TreeView 
ItemsSource="{Binding TreeNodes}"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"/>


There are a number of workarounds for this challenge (and it is a challenge – just try a Google search for Silverlight TreeView SelectedItem) but most of the solutions I’ve read either seem too complex to use, require you to compromise the MVVM pattern, or are unfriendly with Blend. Wouldn’t it be nice if you could just attach a behavior to the TreeView to make it work?


BindableTreeViewSelectedItemBehavior


Yeah – it’s a long name for a behavior, but at least you know what it does just by reading its name. Using it in Blend is pretty easy:


Using the Behavior in Blend


Just drop it on the TreeView control and set its Binding property.The XAML binding will look like this:


<sdk:TreeView ItemsSource="{Binding TreeNodes}" ...>
<i:Interaction.Behaviors>
<Behaviors:BindableTreeViewSelectedItemBehavior
Binding="{Binding SelectedNode}"/>
</i:Interaction.Behaviors>
</sdk:TreeView>


You don’t need to make it a two-way binding, the behavior looks after that for you. Here is a sample application that has two TreeView controls bound to the same data context. If you change the selected item on one TreeView it will update the selected item on the other TreeView:



Under The Hood


This attached behavior works by acting as a go-between using the TreeView.SelectedItemChanged event, the TreeView.SelectedItem property, and a private two-way binding on the data context. Here is a diagram that shows how the behavior wires itself up:


Diagram of relationships for behavior


Click on the diagram to see a larger version. The diagram can be broken down as follows:



  • The behavior examines it’s Binding property and uses that information to create a private two-way binding between the property on the data context of the tree view (in our example it’s against the SelectedNode property) and the behavior’s private SelectedItemProperty DependencyProperty.
  • When the value for the DataContext’s SelectedNode property changes, the change event for the SelectedItemProperty DependencyProperty (AssociatedObjectSelectedItemChanged) fires and we set the new value on the TreeView using the TreeView.SelectedItem property. This allows the tree view to have its selected item set from the data context.
  • The behavior also attaches an event handler for the TreeView.SelectedItemChanged event (also called SelectedItemChanged) which fires whenever the user changes the selected item.
  • When the SelectedItemChanged event handler is called, the behavior updates the value of its SelectedItemProperty DependencyProperty. This allows the data context to have its SelectedNode value updated when the user changes the selected item in the TreeView.

Summary


In this post I described an attached behavior that lets you achieve two-way binding on the TreeView.SelectedItem property. The behavior is especially useful if you are using the MVVM pattern to keep your views and view models separate, and want to avoid code in the View’s code-behind file. The source can be downloaded here.

13 comments:

  1. A bit new to WPF/Silverlight, and I like this behavior, and the idea of this behavior(yay MVVM!). I notice it has a dependency on the Silverlight Toolkit re: TreeView.ExpandAll & a few other methods(noticed mostly 'cause I didn't have it installed).

    What does that mean in terms of using this behavior as-is in WPF apps?

    My goal is to demo/prototype things in sketchflow/silverlight for ease of distribution of the prototypes, and then use WPF for the real deal since our real deal does not include silverlight @ the moment.

    Thanks!

    ReplyDelete
  2. wtj - I don't do much with WPF but I would guess you would have to provide your own extension method for it. So the short answer is, it probably won't work as-is in WPF.

    I would use Reflector to examine the implementation of the extension method and see if it can be implemented similarly for WPF.

    ReplyDelete
  3. Thanks Phil,

    It wouldn't be too hard to port, I'm sure. Thanks for posting!

    -Wes

    ReplyDelete
  4. Phil, have been completely frustrated trying to find a solution to this SelectedNode/MVVM problem for months, and I think this is 99.9% what I need. Problem is, when I use this method, and in my View Model I set the SelectedNode in code (i.e., this.SelectedNoe = nodeToSelect), it looks like it goes through and sets it, but then the process repeats and the original node gets set back. At least, when I have breakpoints set on the SelectedNode setter that is what is happening, although when setting no breakpoints it still appears as though (visually) the new item is selected, but the selection highlight is very very faint, not like it is when clicking directly on the item. Any suggestions?

    ReplyDelete
  5. njm,
    It sounds like you may have something else going on - is the SelectedNode on the VM or the SelectedItem on the TreeView bound to anything else? Have you got some code I could look at?

    ReplyDelete
  6. Nice behaviour, but when the tree is large, the performance suffers. This is because ExpandAll(); and CollapseAllButSelectedPath(); are used for each selection.
    To overcome this, have you considered a solution such as: http://blogs.msdn.com/b/wpfsdk/archive/2010/02/23/finding-an-object-treeviewitem.aspx

    ReplyDelete
  7. Anonymous, thanks for the link - unfortunately, based on the comments on the article, it looks like that solution also suffers from performance issues with a large data set.

    The solution does not work with Silverlight anyway since it relies on TreeViewItem.BringIntoView() and a couple of other WPF calls which are not present in the Silverlight implementation.

    Additionally, it requires the use of a custom VirtualizingStackPanel which ruins the Blend design experience.

    The TreeView is a frustrating control to work with. I'm aware of the performance issues with the control, but have not found a solution that doesn't involve creating my own subclass.

    ReplyDelete
  8. Hi Phil,

    It might sound like a silly question, but since I’d like to use your behavior source code as-is in my (commercial) application, is there any license agreement I must follow to do so?

    If there is, can you point to it so that I can copy-paste it to my documentation?
    If not, please confirm so…

    Thanks a lot for publishing this stuff,
    (and by the way, I was also born in NZ/Wellington though not living there anymore :)

    Ziev

    ReplyDelete
  9. Hi Ziev, thanks for asking. Feel free to use it for any commercial application.

    The only restriction I would place on it would be that it is not sold as-is (a behavior) or as part of a library of behaviors/controls etc.

    I'm sure there's a standard license around somewhere that covers that but I have no problems with you using it in your app.

    ReplyDelete
  10. Thanks for providing this solution. I tried a lot of workarounds for selecting an item in treeview, but this is the only one that works (for me).

    ReplyDelete
  11. Thanks for the feedback Jehof. Glad it works for you.

    ReplyDelete
  12. Great behaviour, works well.

    Small bug, though: the behaviour, as-is, doesn't support RelativeSource or ElementName bindings. It's easy to fix: when the behaviour is creating a new binding based on the developer-provided one, it should set one of the Source, RelativeSource, or ElementName for the new binding, depending on which are available (Source is always available, so should be the default case).

    ReplyDelete
    Replies
    1. Thanks for the feedback Duncan.

      I'm not sure I see how binding the selected item to another element's property would work though, since SelectedItem returns the data item, not the treeview item. How does another element become the data context for a treeview item?

      Perhaps you could write a blog showing the change?

      Delete