Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Advanced WPF TreeView in C#/VB.Net Part 6 of n

0.00/5 (No votes)
11 Jan 2018 2  
Tips & Tricks on using checkboxes within a WPF treeview.

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:

Demo Screenshot

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):

Demo Screenshot Demo Screenshot Demo Screenshot
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:

  1. A Level-Order traversal algorithm beginning at the checked/unchecked node (either checking or unchecking the selected node and all its children),
  2. 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.

Class Diagram

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);

    // All children of the checked/unchecked item have to assume it's state
    foreach (var item in items)
    {
        var node = item.Node as FooViewModel;
        node.IsChecked = changedItem.IsChecked;
    }
    
    // Visit each parent in turn and determine their correct states
    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)
      {
        // Two different child states found for this parent item ...
        item.IsChecked = null;
        return;
      }
    }
    
    // All child items have the same state as the first child
    item.IsChecked = firstChild;
}
Private Sub CheckItemCommand_Executed(ChangedItem As IFooViewModel)
    Dim items = TreeLib.BreadthFirst.Traverse.LevelOrder(Of IFooViewModel)(ChangedItem.Children, Function(i) i.Children)

    '' All children of the checked/unchecked item have to assume it's state
    For Each item In items
        Dim node = TryCast(item.Node, FooViewModel)
        node.IsChecked = ChangedItem.IsChecked
    Next

    '' Visit each parent in turn And determine their correct states
    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

            '' Two different child states found for this parent item ...
            item.IsChecked = Nothing
            Return
        End If
    Next

    '' All child items have the same state as the first child
    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.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here