Introduction
This article describes a solution to display and manage checkboxes in each tree view item of a WPF tree view. The discussed solution is closely related to the article and solution described by Josh Smith in 2008 [1], (which is almost 10 years ago). The WPF world has moved a little since then and there are some details we can implement slightly differently these days (mostly due to the work and findings that Josh Smith had contributed). This article is an attempt in describing an updated version with visual aids that should help the newbie to better understand how the solution works.
Background
A CodeProject reader asked to present a VB.Net solution that shows how check boxes can be used to manage items in a WPF Tree View. I found the solution by Josh Smith and cannot really find anything wrong with it, other than that it is not in VB.Net and there are some minor details that I would describe slightly different to help the novice understanding the concept.
The screenshots below give you an impression of the use case we are trying to solve here. The application starts-up with the first item selected and you can use the keyboard to navigate the tree and check or uncheck items using the space or enter key:
The next screenshots below show the check state of all items in the tree view when the user checks the corresponding item (after starting the application):
| | |
Checks root item
(Weapons) | Check item in 2nd level
(Vehicles) | Check item in 3rd level
(Submarine) |
We can see here that checking a node does not only influence that nodes state but also the state of the parent and child items. Here is the list of requirements that Josh Smith [1], used to define this behavior in text:
Requirement 1: Each item in the tree must display a checkbox that displays the text and check state of an underlying data object.
Requirement 2: Upon an item being checked or unchecked, all of its child items should be checked or unchecked, respectively.
Requirement 3: If an item’s descendants do not all have the same check state, that item’s check state must be ‘indeterminate.’
Requirement 4: Navigating from item to item should require only one press of an arrow key.
Requirement 5: Pressing the Spacebar or Enter keys should toggle the check state of the selected item.
Requirement 6: Clicking on an item’s checkbox should toggle its check state, but not select the item.
Requirement 7: Clicking on an item’s display text should select the item, but not toggle its check state.
Requirement 8: All items in the tree should be in the expanded state by default.
A careful read of these requirements (2 and 3) shows that we have to visit far more nodes in the tree to make sure that all details are full-filled. That is, the requested behavior requires a navigational concept that navigates also over the parents of a tree item (and not just over a current node and its children as pointed out earlier [4]).
The above animation visualizes a tree view in which no item is checked in frame 0 (black color -> not checked). The user clicks on node b and the system visits node b in frame 1 to set the checked state (green color -> checked). Each later frame 2-6 shows how the system visits each child node and sets the checked state.
The image in frame 7 shows that the system also visits the parent of a checked item (in this case the root item a to verify whether the state of the parent item is also consistent with the state of the newly checked item b The answer to this question can be determined by looking at the children c and d of item a in frames 8-9.
The correct state for item a is 'indeterminate' (yellow) since some of its children are checked (green) and some of its children are unchecked (black). The algorithm can end here (frame 11) because node a does not have a parent node and the nodes c and d did not change their state.
The algorithm would have to visit further parent nodes if item a would have a parent. But the children of nodes c and d are never required for a visit in this case since their cannot be a situation in which c or d would have to change its state.
The above algorithm is required to process a check operation on item x. The Level-Order traversal of the children of item x is not necessary since it has no children. Traversing the parents, however, is still required and yields the 'indeterminate' state for items a and b since their children do have different (checked and unchecked) states.
We are now ready to summarize that the required algorithm needs to implement:
- A Level-Order traversal algorithm beginning at the checked/unchecked node (either checking or unchecking the selected node and all its children),
- and visit all parent nodes of the selected node to re-evaluate their states based on the combined state of each parents children nodes.
Lets now turn to the code to investigate a possible WPF implementation of that challenge.
Using the code
The attached samples in C# and VB.Net are as usual instanciated via the Window1
object - see the code behind to determine how the AppViewModel
object is created and attached to the Window1
's DataContext
property. This mechanism binds all other bindings in the Window1
's XAML file to the properties of the AppViewModel
object. This includes properties that are contained in an object that is instanciated and exposed within the AppViewModel
object (e.g.; Root property binds to ItemsSource
in Window1
's TreeView).
This binding process cascades even further into the HierarchicalDataTemplate
that is used to instanciate and bind each tree view item. A tree view item is of course only instanciated, if the Children
collection in the FooViewModel
class contains an actual object.
Another good birds eye view is the class diagram of the attached project. The diagram can be generated with Visual Studio and shows the ViewModels
, View (Window1
), and interfaces
that drive the application.
The CheckItemCommand
in the AppViewModel
implements the previously mentioned traversal algorithm to the selected child items and its parents. An insection of the Window1.xmal shows that the CheckItemCommand
is bound to the CheckBox
in the HierarchicalDataTemplate
and the VirtualToggleButton
attached behavior class. This is how the CheckBox
directly forwards the event of being checked or uncheck with the mouse, while the VirtualToggleButton
class relays the same event, originating from the keyboard, into the AppViewModel
object.
The AppViewModel.CheckItemCommand
invokes the following code to process the event of a checkbox being toggled:
private void CheckItemCommand_Executed(IFooViewModel changedItem)
{
var items = TreeLib.BreadthFirst.Traverse.LevelOrder
<IFooViewModel>(changedItem.Children, i => i.Children);
foreach (var item in items)
{
var node = item.Node as FooViewModel;
node.IsChecked = changedItem.IsChecked;
}
var parentItem = changedItem.Parent;
for( ; parentItem != null; parentItem = parentItem.Parent)
{
ResetParentItemState(parentItem as IFooViewModel);
}
}
private void ResetParentItemState(IFooViewModel item)
{
if (item == null)
return;
if (item.ChildrenCount == 0)
return;
var itemChildren = item.Children.ToArray();
bool? firstChild = itemChildren[0].IsChecked;
for(int i=1; i< itemChildren.Length; i++)
{
if (Object.Equals(firstChild, itemChildren[i].IsChecked) == false)
{
item.IsChecked = null;
return;
}
}
item.IsChecked = firstChild;
}
Private Sub CheckItemCommand_Executed(ChangedItem As IFooViewModel)
Dim items = TreeLib.BreadthFirst.Traverse.LevelOrder(Of IFooViewModel)(ChangedItem.Children, Function(i) i.Children)
For Each item In items
Dim node = TryCast(item.Node, FooViewModel)
node.IsChecked = ChangedItem.IsChecked
Next
Dim parentItem = ChangedItem.Parent
While parentItem IsNot Nothing
ResetParentItemState(TryCast(parentItem, IFooViewModel))
parentItem = parentItem.Parent
End While
End Sub
Private Sub ResetParentItemState(item As IFooViewModel)
If (item Is Nothing) Then
Return
End If
If item.ChildrenCount = 0 Then
Return
End If
Dim itemChildren = item.Children.ToArray()
Dim firstChild As Boolean?
firstChild = itemChildren(0).IsChecked
For i = 0 To itemChildren.Length - 1
If (Object.Equals(firstChild, itemChildren(i).IsChecked) = False) Then
item.IsChecked = Nothing
Return
End If
Next
item.IsChecked = firstChild
End Sub
The above code in the CheckItemCommand_Executed
method is invoked with the ChangedItem
parameter which represents the item whos checkbox has just been toggled. This code implements the previously [4] described Level-Order traversal via TreeLib library on the checked/unchecked item. The later loop and invocation of the ResetParentItemState
method implements the re-evaluation and traversal of all parent items.
Both modes of interaction, CheckBox
and VirtualToggleButton
attached behavior class, invoke the same code, which makes the heart of the action surprisingly simple.
Conclusions
The code in this article contains a refreshed version of another great article by Josh Smith. I hope the extra work for the animations and slightly changed implementation was helpful for those who code and still have problems getting their head around the non-trivial concept called MVVM/WPF in relation to the tree view control.
Please do have a look at the source code since I tried to include comments virtually everywhere.
You still have questions? Then waste no time and let me know about your feedback.