Making a Two-Way binding for SelectedItem on a WPF TreeView

Anyone who has experience with WPF probably worked with the TreeView control at some point. Unlike almost every other standard WPF control that derives from ItemsControl, the TreeView does not let you bind the SelectedItem property, in fact that property is get-only! Even in a simplest case, where you only need to update the SelectedItem in the view model in a OneWayToSource manner, you’d have to either handle the SelectedItemChanged event or use an event-to-command routing. And if your case involves two-way binding, so that the view can be updated from the view model, the solution gets very untrivial.


  1. Handle events

The most basic approach you can take to solve this – is to use the code-behind to handle the PropertyChanged event of the view model and SelectedItemChanged of the TreeView to keep the SelectedItem in sync.

A pretty brain-dead solution, forces you to add a bunch of junk in your code-behind and scales terribly as you add more TreeView controls.

Pros:

  • Easy and fast to implement
  • MVVM-friendly

Cons:

  • A lot of code in the code-behind
  • If this needs to work on multiple TreeView controls, you’d also need to copy paste a lot of code
  • If the new SelectedItem is actually invisible (the parent nodes are not expanded) then no changes will be observed in the view (!!!)
  1. IsSelected property inside the model class

The other solution, particularly popular on StackOverflow – is to add an IsSelected property to the model class and then bind it in the TreeViewItem (the container control). Explained better here.

Pros:

  • No code-behind at all
  • Scales well
  • Easy to implement

Cons:

  • Adding IsSelected property breaks View/Model decoupling (!!!)
  • If the SelectedItem is not visible (parents not expanded), no change in the view will be observed (!!!)

Notice how both of the above methods fail on one very important requirement. If the SelectedItem changes from the view model (not by the user clicking, but from the code), it’s natural to expect the TreeView to navigate to that item, even if it’s hidden somewhere deep inside the hierarchy of non-expanded nodes. Like shown here:

Well the problem is, even though you can get the TreeViewItem controls that wrap around the DataTemplates, they only actually exist when they are visible. In other words, you can’t obtain TreeViewItem references for children of nodes that are not yet expanded.

To work around this, you have to expand parent nodes one-by-one by setting IsExpanded=true until the target is reached, and only then set IsSelected=true. Once a node is expanded, it will not immediately become accessible, but you can handle the Loaded event to process it as soon as it’s created. In the event handler itself, it is possible to continue that recursive logic which will end once the node you’re looking for has been found.

The workflow:

  • Get item containers for top-most level nodes
  • Iterate through containers, set IsSelected=true if its DataContext equals to the SelectedItem or set IsExpanded=true if its DataContext is parent to SelectedItem
  • In case the selected model is found – the workflow breaks here
  • If it isn’t, at least one node should have been expanded (the parent to the selected model), which in turn will fire its own Loaded event
  • Handle the Loaded event, get the child containers of that TreeViewItem (possible since it’s now expanded) and loop back to #2

To register a handler for the Loaded event, I can use the ItemContainerStyle to substitute a style that will have a wired event handler.

  1. Using behavior

I created a behavior that implements and encapsulated all the logic above, which can be attached to any TreeView and configured as seen necessary.

The source code for my implementation goes like this:

Notice how I’m using the _modelHandled flag to prevent event loops that would otherwise occur.

I’ve also added two properties – HierarchyPredicate and ExpandSelected.

Since the behavior deals with unknown instances, boxed into object type, it has no way of knowing whether one model is a child of another model. That’s where HierarchyPredicate steps in – it’s a delegate that will return whether one object is a child of another object. However, if the predicate is not set, the behavior resorts to a fallback – it considers all objects to be relatives (parents and children of each other), which effectively means that when a SelectedItem is changed from view model, all nodes will be expanded. This makes sure that the behavior works (although not optimally) even if it wasn’t correctly configured.

ExpandSelected is a bool property which tells the behavior whether or not the selected node should also be expanded.

Finally, this is how you use this behavior in XAML:

Pros:

  • MVVM-friendly
  • UI actually reflects changes in the view model, expanding nodes as necessary
  • Scales well
  • Very easy to customize by updating the behavior

Cons:

  • None 🙂

This behavior, among others, is implemented in my WPF extension library.

Comments