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

Working with Checkboxes in the WPF TreeView

0.00/5 (No votes)
1 Aug 2008 7  
Examines how to create a tree of checkboxes that intelligently update their own check state and are easy to navigate with the keyboard

Introduction

This article reviews a WPF TreeView whose items contain checkboxes. Each item is bound to a ViewModel object. When a ViewModel object’s check state changes, it applies simple rules to the check state of its parent and child items. This article also shows how to use the attached behavior concept to turn a TreeViewItem into a virtual ToggleButton, which helps make the TreeView’s keyboard interaction simple and intuitive.

This article assumes that the reader is already familiar with data binding and templates, binding a TreeView to a ViewModel, and attached properties.

Background

It is very common to have a TreeView whose items are checkboxes, such as when presenting the user with a hierarchical set of options to select. In some UI platforms, such as WinForms, the standard TreeView control offers built-in support for displaying checkboxes in its items. Since element composition and rich data binding are two core aspects of WPF, the WPF TreeView does not offer intrinsic support for displaying checkboxes. It is very easy to declare a CheckBox control in a TreeView’s ItemTemplate and suddenly every item in the tree contains a CheckBox. Add a simple {Binding} expression to the IsChecked property, and suddenly the check state of those boxes is bound to some property on the underlying data objects. It would be superfluous, at best, for the WPF TreeView to have an API specific to displaying checkboxes in its items.

The Devil is in the Details

This sounds too good to be true, and it is. Making the TreeView “feel right,” from a keyboard navigation perspective, is not quite as simple. The fundamental problem is that as you navigate the tree via arrow keys, a TreeViewItem will first take input focus, and then the CheckBox it contains will take focus upon the next keystroke. Both the TreeViewItem and CheckBox controls are focusable. The result is that you must press an arrow key twice to navigate from item to item in the tree. That is definitely not an acceptable user experience, and there is no simple property that you can set to make it work properly. I have already brought this issue to the attention of a certain key member on the WPF team at Microsoft, so they might address it in a future version of the platform.

Functional Requirements

Before we start to examine how this demo program works, first we will review what it does. Here is a screenshot of the demo application in action:

screenshot.png

Now let’s see what the functional requirements are:

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.

I suggest you copy those requirements and paste them into your favorite text editor, such as Notepad, because we will reference them throughout the rest of the article by number.

Putting the Smarts in a ViewModel

As explained in my ‘Simplifying the WPF TreeView by Using the ViewModel Pattern’ article, the TreeView was practically designed to be used in conjunction with a ViewModel. This article takes that idea further, and shows how we can use a ViewModel to encapsulate application-specific logic related to the check state of items in the tree. In this article, we will examine my FooViewModel class, which the following interface describes:

interface IFooViewModel : INotifyPropertyChanged
{
    List<FooViewModel> Children { get; }
    bool? IsChecked { get; set; }
    bool IsInitiallySelected { get; }
    string Name { get; }
}

The most interesting aspect of this ViewModel class is the logic behind the IsChecked property. This logic satisfies Requirements 2 and 3, seen previously. The FooViewModel’s IsChecked logic is below:

/// <summary>
/// Gets/sets the state of the associated UI toggle (ex. CheckBox).
/// The return value is calculated based on the check state of all
/// child FooViewModels.  Setting this property to true or false
/// will set all children to the same check state, and setting it 
/// to any value will cause the parent to verify its check state.
/// </summary>
public bool? IsChecked
{
    get { return _isChecked; }
    set { this.SetIsChecked(value, true, true); }
}

void SetIsChecked(bool? value, bool updateChildren, bool updateParent)
{
    if (value == _isChecked)
        return;

    _isChecked = value;

    if (updateChildren && _isChecked.HasValue)
        this.Children.ForEach(c => c.SetIsChecked(_isChecked, true, false));

    if (updateParent && _parent != null)
        _parent.VerifyCheckState();

    this.OnPropertyChanged("IsChecked");
}

void VerifyCheckState()
{
    bool? state = null;
    for (int i = 0; i < this.Children.Count; ++i)
    {
        bool? current = this.Children[i].IsChecked;
        if (i == 0)
        {
            state = current;
        }
        else if (state != current)
        {
            state = null;
            break;
        }
    }
    this.SetIsChecked(state, false, true);
}

This strategy is specific to the functional requirements I imposed upon myself. If you have different rules regarding how and when items should update their check state, simply adjust the logic in those methods to suit your needs.

TreeView Configuration

Now it is time to see how the TreeView is able to display checkboxes and bind to the ViewModel. This is entirely accomplished in XAML. The TreeView declaration is actually quite simple, as seen below:

<TreeView 
  x:Name="tree"
  ItemContainerStyle="{StaticResource TreeViewItemStyle}"
  ItemsSource="{Binding Mode=OneTime}"
  ItemTemplate="{StaticResource CheckBoxItemTemplate}"
  />

The TreeView’s ItemsSource property is implicitly bound to its DataContext, which inherits a List<FooViewModel> from the containing window. That list only contains one ViewModel object, but it is necessary to put it into a collection because ItemsSource is of type IEnumerable.

TreeViewItem is a container of visual elements generated by the ItemTemplate. In this demo, we assign the following HierarchicalDataTemplate to the tree's ItemTemplate property:

<HierarchicalDataTemplate 
  x:Key="CheckBoxItemTemplate"
  ItemsSource="{Binding Children, Mode=OneTime}"
  >
  <StackPanel Orientation="Horizontal">
    <!-- These elements are bound to a FooViewModel object. -->
    <CheckBox
      Focusable="False" 
      IsChecked="{Binding IsChecked}" 
      VerticalAlignment="Center"
      />
    <ContentPresenter 
      Content="{Binding Name, Mode=OneTime}" 
      Margin="2,0"
      />
  </StackPanel>
</HierarchicalDataTemplate>

There are several points of interest in that template. The template includes a CheckBox whose Focusable property is set to false. This prevents the CheckBox from ever receiving input focus, which assists in meeting Requirement 4. You might be wondering how we will be able to satisfy Requirement 5 if the CheckBox never has input focus. We will address that issue later in this article, when we examine how to attach the behavior of a ToggleButton to a TreeViewItem.

The CheckBox’s IsChecked property is bound to the IsChecked property of a FooViewModel object, but notice that its Content property is not set to anything. Instead, there is a ContentPresenter directly next to it, whose Content is bound to the Name property of a FooViewModel object. By default, clicking anywhere on a CheckBox causes it to toggle its check state. By using a separate ContentPresenter, rather than setting the CheckBox’s Content property, we can avoid that default behavior. This helps us satisfy Requirements 6 and 7. Clicking on the box element in the CheckBox will cause its check state to change, but clicking on the neighboring display text will not. Similarly, clicking on the box in the CheckBox will not select that item, but clicking on the neighboring display text will.

We will examine the TreeView’s ItemContainerStyle in the next section.

Turning a TreeViewItem into a ToggleButton

In the previous section, we quickly considered an interesting question. If the CheckBox in the TreeViewItem has its Focusable property set to false, how can it toggle its check state in response to the Spacebar or Enter key? Since an element only receives keystrokes if it has keyboard focus, it seems impossible for Requirement 5 to be satisfied. Keep in mind; we had to set the CheckBox’s Focusable property to false so that navigating from item to item in the tree does not require multiple keystrokes.

This is a tricky problem: we cannot let the CheckBox ever have input focus because it negatively affects keyboard navigation, yet, when its containing item is selected, it must somehow toggle its check state in response to certain keystrokes. These seem to be mutually exclusive requirements. When I hit this brick wall, I decided to seek geek from the WPF Disciples, and started this thread. Not to my surprise, Dr. WPF had already encountered this type of problem and devised a brilliant-approaching-genius solution that was easy to plug into my application. The good Doctor sent me the code for a VirtualToggleButton class, and was kind enough to allow me to publish it in this article.

The Doctor’s solution uses what John Gossman refers to as “attached behavior.” The idea is that you set an attached property on an element so that you can gain access to the element from the class that exposes the attached property. Once that class has access to the element, it can hook events on it and, in response to those events firing, make the element do things that it normally would not do. It is a very convenient alternative to creating and using subclasses, and is very XAML-friendly.

In this article, we see how to give a TreeViewItem an attached IsChecked property that toggles when the user presses the Spacebar or Enter key. That attached IsChecked property binds to the IsChecked property of a FooViewModel object, which is also bound to the IsChecked property of the CheckBox in the TreeViewItem. This solution gives the appearance that a CheckBox is toggling its check state in response to the Spacebar or Enter key, but in reality, its IsChecked property updates in response to a TreeViewItem pushing a new value to the ViewModel’s IsChecked property via data binding.

Before going any further, I should point out that I fully recognize that this is crazy. The fact that this is the cleanest way to implement a TreeView of checkboxes in WPF v3.5 indicates, to me, that Microsoft needs to simplify this aspect of the platform. However, until they do, this is probably the best way to implement the feature.

In this demo, we do not make use of all features in Dr. WPF’s VirtualToggleButton class. It has support for several things that we do not need, such as handling mouse clicks and providing tri-state checkboxes. We only need to make use of its support for the attached IsVirtualToggleButton and IsChecked properties and the keyboard interaction behavior it provides.

Here is the property-changed callback method for the attached IsVirtualToggleButton property, which is what enables this class to gain access to TreeViewItems in the tree:

/// <summary>
/// Handles changes to the IsVirtualToggleButton property.
/// </summary>
private static void OnIsVirtualToggleButtonChanged(
  DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    IInputElement element = d as IInputElement;
    if (element != null)
    {
        if ((bool)e.NewValue)
        {
            element.MouseLeftButtonDown += OnMouseLeftButtonDown;
            element.KeyDown += OnKeyDown;
        }
        else
        {
            element.MouseLeftButtonDown -= OnMouseLeftButtonDown;
            element.KeyDown -= OnKeyDown;
        }
    }
}

When a TreeViewItem raises its KeyDown event, this logic executes:

private static void OnKeyDown(object sender, KeyEventArgs e)
{
    if (e.OriginalSource == sender)
    {
        if (e.Key == Key.Space)
        {
            // ignore alt+space which invokes the system menu
            if ((Keyboard.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt) 
                return;

            UpdateIsChecked(sender as DependencyObject);
            e.Handled = true;
        }
        else if (e.Key == Key.Enter && 
         (bool)(sender as DependencyObject)
         .GetValue(KeyboardNavigation.AcceptsReturnProperty))
        {
            UpdateIsChecked(sender as DependencyObject);
            e.Handled = true;
        }
    }
}

private static void UpdateIsChecked(DependencyObject d)
{
    Nullable<bool> isChecked = GetIsChecked(d);
    if (isChecked == true)
    {
        SetIsChecked(d, 
         GetIsThreeState(d) ? 
         (Nullable<bool>)null : 
         (Nullable<bool>)false);
    }
    else
    {
        SetIsChecked(d, isChecked.HasValue);
    }
}

The UpdateIsChecked method sets the attached IsChecked property on an element, which is a TreeViewItem in this demo. Setting an attached property on a TreeViewItem has no effect by itself. In order to have the application use that property value, it must be bound to something. In this application, it is bound to the IsChecked property of a FooViewModel object. The following Style is assigned to the TreeView’s ItemContainerStyle property. It ties a TreeViewItem to a FooViewModel object and adds the virtual ToggleButton behavior that we just examined.

<Style x:Key="TreeViewItemStyle" TargetType="TreeViewItem">
  <Setter Property="IsExpanded" Value="True" />
  <Setter Property="IsSelected" Value="{Binding IsInitiallySelected, Mode=OneTime}" />
  <Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
  <Setter Property="dw:VirtualToggleButton.IsVirtualToggleButton" Value="True" />
  <Setter Property="dw:VirtualToggleButton.IsChecked" Value="{Binding IsChecked}" />        
</Style>

This piece ties the entire puzzle together. Note that the attached KeyboardNavigation.AcceptsReturn property is set to true on each TreeViewItem so that the VirtualToggleButton will toggle its check state in response to the Enter key. The first Setter in the Style, which sets the initial value of each item's IsExpanded property to true, ensures that Requirement 8 is met.

CheckBox Bug in Aero Theme

I must point out one strange, and disappointing, issue. The Aero theme for WPF’s CheckBox control has a problem in .NET 3.5. When it moves from the ‘Indeterminate’ state to the ‘Checked’ state, the background of the box does not update properly until you move the mouse cursor over it. You can see this in the screenshot below:

screenshot_aero.png

To workaround this, I merged the Royale theme into the window’s Resources collection. The CheckBox does not exhibit this defect when using the Royale theme. I really hope Microsoft fixes this in the next version of WPF.

Revision History

  • August 1, 2008 – Created the article

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