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.
- 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.
- Easy and fast to implement
- A lot of code in the code-behind
- If this needs to work on multiple
TreeViewcontrols, you’d also need to copy paste a lot of code
- If the new
SelectedItemis actually invisible (the parent nodes are not expanded) then no changes will be observed in the view (!!!)
- 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.
- No code-behind at all
- Scales well
- Easy to implement
IsSelectedproperty breaks View/Model decoupling (!!!)
- If the
SelectedItemis 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.
- Get item containers for top-most level nodes
- Iterate through containers, set
DataContextequals to the
DataContextis parent to
- 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
- Handle the
Loadedevent, 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.
- 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 –
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:
- UI actually reflects changes in the view model, expanding nodes as necessary
- Scales well
- Very easy to customize by updating the behavior
- None 🙂
This behavior, among others, is implemented in my WPF extension library.