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

A Versatile TreeView for WPF

0.00/5 (No votes)
7 Feb 2008 90  
A strongly typed enhancement of the regular WPF TreeView control.

Introduction

I was recently working on a WPF project, and it was all about data editing and synchronization. Data was hierarchically organized, and I had to provide a lot of different views on isolated and merged data sources based on a variety of external factors.

First of all: I 'love' data binding in WPF — it just makes a lot of things so easy! So in order to display nested data on a TreeView control, I started with a hierarchical data template. However — I soon reached limits that proved hard to get around. While data binding had served me very well so far, I just needed more control from code when it came to that tree. Furthermore, I faced a few other issues when the tree's data source was refreshed or replaced, which caused nasty side effects with TwoWay data binding.

Bottom line was that the purely data-driven UI didn't quite work in my case — I needed an alternative. The result was a composite control I built around TreeView. This solution took care of my issues (at least the tree-related ones while I could still rely on the basic functionality of TreeView without having to reinvent the wheel. The result was simple yet effective, so I decided to extend it a little in order to get a generic control which I could reuse and share with the community. Which is why I have to write that darn article now...

Contents

Features / Sample Application

Here's the most important features at a glance:

  • You can tweak the tree for maximum performance, minimum memory footprint, or even a fully virtualized solution that gets data on demand and keeps it cached until you decide to remove it explicitly.
  • The tree provides simple and type-safe access to the underlying data (no properties of type object here). And as opposite to the standard TreeView, the SelectedItem property is not read-only.
  • Simple styling and sorting.
  • Internally, there is a clear distinction between UI (tree nodes) and bound items. This resolves reloading and focus issues with the control. You can get a tree node (UI) by a bound item (data) and vice versa, or recursively iterate through all your tree nodes.
  • Various extension points — a lot of code is virtual and can be overridden in order to control the behavior of the tree — down to the creation of single TreeViewItem instances.
  • Simplified context menu handling.
  • The tree layout (expanded/selected nodes) can be saved and reapplied.

Pretty much everything I am going to write here is illustrated in the sample application that comes with the download. If you're already tired of reading, you might just download the sample, play around, and come back later if you need more information.

sampleapp-thumb.png

Implementing your Tree Control

Regarding terms: Actually, this is the "hardcodet.net WPF TreeView Control," as I published the initial version on my blog. However, I won't write that all over the place because that just sounds horribly narcistic, and it doesn't really make sense here at CP anyway. As I'll refer to WPF's built-in TreeView control a lot, I'll call it V-Tree throughout this article (which is completely random, btw).

Extending TreeViewBase<T>

All the features of V-Tree are being provided by the abstract TreeViewBase<T> class. That's abstract like in: You will have to write some code (gasp!) in order to getting started. However, chances are high that you'll stay below 10 lines of code, as the base class just needs to know three things:

  1. How to generate a unique identifier for a given tree node: GetItemKey
  2. How to get a bound item's childs, if there are any available: GetChildItems
  3. How to get a bound item's parent, if there is one: GetParentItem

There are quite a few virtual methods you can override in order to control the tree's behavior, but these three abstract methods may well be everything you'll ever need. Below is the implementation of the sample project's CategoryTree control, which operates upon ShopCategory objects. As you can see, CategoryTree manages with 3 lines of code:

//a tree control that handles ShopCategory objects
public class CategoryTree : TreeViewBase<ShopCategory>
{
  //the sample uses the category's name as the identifier
  public override string GetItemKey(ShopCategory item)
  {
    return item.CategoryName;
  }

  //returns subcategories that should be available through the tree

  public override ICollection<ShopCategory> GetChildItems(ShopCategory parent)
  {
    return parent.SubCategories;
  }

  //get the parent category, or null if it's a root category
  public override ShopCategory GetParentItem(ShopCategory item)
  {
    return item.ParentCategory;
  }
}

Hint: Make sure your tree control class is public. Otherwise, Intellisense in Visual Studio might not work properly.

What if I want to bind items of various types to the tree?

If you want to bind heterogeneous data to the tree, chances are high that your bound items do have something in common, and having them implement a common interface or derive from a custom base class might be a good design decision anyway. However, nobody keeps you from implementing a tree control that works on items of type object which allows you to throw pretty much everything at it. It just wouldn't be that convenient anymore:

//A tree that supports completely different items.

public class MyObjectTree : TreeViewBase<object>
{
  public override string GetItemKey(object item)
  {
    if (item is ShopCategory)
    {
      return ((ShopCategory) item).CategoryName;
    }
    else if (item is Vendor)
    {
      return ((Vendor)item).VendorId;
    }
    else

    {
      ...
    }
  }

  public override ICollection<object> GetChildItems(object parent)
  {
    //some implementation

  }

  public override object GetParentItem(object item)
  {
    //some implementation

  }
}

Basic Structure

All code samples I am going to use refer to the sample implementation of the abstract TreeViewBase control, the CategoryTree class. As you can see in the diagram below, the tree's Items collection is bound to an ObservableCollection of type ShopCategory. When it comes to rendering the bound data on the UI, TreeViewBase uses a regular TreeView control, and creates a TreeViewItem instance for every bound ShopCategory:

treemodel.png

shopcategory.png

Working with the Control

This section covers the features of the control. As a reference, below is a sample declaration of the tree. Note that some of the properties below are redundant (e.g. IsLazyLoading, which is true by default).

<local:CategoryTree x:Name="MyTree"
                    Items="{Binding Source={StaticResource Shop}, Path=Categories}"

                    IsLazyLoading="True"
                    ObserveChildItems="True"
                    ClearCollapsedNodes="True"
                    AutoCollapse="True"

                    RootNode="{StaticResource CustomRootNode}"

                    NodeContextMenu="{StaticResource CategoryMenu}"
                    TreeNodeStyle="{StaticResource SimpleFolders}"
                    TreeStyle="{StaticResource SimpleTreeStyle}"

                    NodeSortDescriptions="{StaticResource AscendingNames}"
                    PreserveLayoutOnRefresh="True"

                    SelectedItem="{Binding ElementName=Foo,
                                   Path=Bar, Mode=TwoWay}"
                    SelectedItemChanged="OnSelectedItemChanged"                 
                    />

Setting the Items Property

In order to display any data, the tree's Items dependency property needs to be set. Items takes an IEnumerable<T>, where T is the type of items your V-Tree implementation handles. In the sample, these are ShopCategory objects. You can set this property in code or XAML:

<local:CategoryTree x:Name="MyTree"
                    Items="{Binding Source={StaticResource Shop}, Path=Categories}"

Items provides the tree with the root items to get started with. The sample application binds to a collection that contains 3 ShopCategory objects:

  • Books
  • Movies
  • Music

What happens now, is this:

  • For every of the three ShopCategory items, the tree creates a corresponding TreeViewItem instance which is assigned to the internal TreeView control.
  • If one of the tree's nodes is being expanded (Books in the screenshot below), the control determines the child categories by invoking GetChildItems, and renders child nodes for every item that is being returned. In the screenshot below, that's the categories Fiction, Politics, and Travel.

It doesn't look too sexy yet, but it's clearly a tree:

unstyledtree.png

Selected Items

Selecting items was a bit cumbersome with the legacy TreeView control, as the SelectedItem property is read-only. However, the V-Tree makes this very easy, as it provides a strongly-typed SelectedItem property that can be set externally:

public virtual T SelectedItem
{
  get { return (T) GetValue(SelectedItemProperty); }
  set { SetValue(SelectedItemProperty, value); }
}

Next to the SelectedItem property, there's a SelectedItemChanged routed event which is being invoked as soon as the selected item changes. The RoutedTreeItemEventArgs provides both the tree's current and the previously selected items. Again, this event makes use of the class' strong typing:

public void Test()
{
  CategoryTree tree = new CategoryTree();
  tree.SelectedItemChanged += Tree_SelectedItemChanged;
}



private void Tree_SelectedItemChanged(object sender, RoutedTreeItemEventArgs e)
{
  //item properties are strongly typed - no conversion needed
  ShopCategory newCategory = e.NewItem;
  ShopCategory oldCategory = e.OldItem;
}

Multiple Selections

Just like the built-in TreeView control, V-Tree doesn't support multiple selections.

Lazy Loading, Caching, and Memory Consumption

Imagine you'd want to display hierarchical data in two different scenarios:

  • Data is locally available, access is very fast. A fully expanded tree contains thousands of nodes.
  • Data is being retrieved from an external web service — a request may take quite some time but there's not too much data.

These scenarios require fundamentally different concepts when it comes to managing tree nodes. In the first scenario, it would be desirable to start with an empty tree, and create tree nodes on demand (lazy loading): As soon as a node is being expanded. If a tree node is being collapsed, all child nodes should be disposed and recreated again if the user re-expands the node.

While lazy loading might be an option for the second scenario as well, disposing data is nothing you would want to do if — refetching the data would be far too expensive. Data that was retrieved from the web service should be cached locally in order to prevent repeated requests for the same data over and over again.

The control provides two dependency properties to support both scenarios:

  • IsLazyLoading: This property defaults to true and tells the tree not to create tree nodes until a node is being expanded. If this property is set to false, the whole tree is being created at once.
  • ClearCollapsedNodes: This property defaults to true as well, and instructs the control to discard nodes as soon as their parent is being collapsed. This keeps the memory footprint at a minimum, but results in GetChildItems being re-invoked if the parent node is expanded again. This property has no effect if IsLazyLoading is false (because this creates the whole tree at once).

Attention: Even if IsLazyLoading is true, the GetChildItems method you implemented is being invoked for every visible node by default, even if it's not yet expanded. This is because of the default implementation of the tree's HasChildItems method, which is invoked in order to determine whether a tree node should be rendered with or without an expander.

protected virtual bool HasChildItems(T parent)
{
  return GetChildItems(parent).Count > 0;
}

However, this only applies for the visible nodes, and does not cause the tree to traverse the whole hierarchy. However, if GetChildItems is really expensive, you should override it. Even just returning true in every case is an option — check the documentation of the method for further details.

AutoCollapse Dependency Property

This boolean dependency property might come in handy if you want the tree to always display the minimum amount of nodes possible. If set to true, the tree collapses all nodes that do not need to be expanded in order to display the currently selected item. Check the sample application in order to see the feature.

Sample

The sample application allows you to set these properties independently and shows the number of tree nodes that are currently in memory at the bottom of the tree control. Below is the same tree twice after a full expansion/collapse cyle: in the first screenshot, ClearCollapsedNodes was set to false, the second screenshot shows the tree with the property set to true. You can imagine that with a huge tree, this makes quite a difference:

lazyloading-off.png rootnode-on.png

Child Collection Monitoring vs. Refresh

Child Collection Monitoring

Per default, the tree tries to monitor the child collections of all existing tree nodes for changes, even if they are being collapsed. This ensures that the UI always reflects the correct state (e.g. by removing the expander of a collapsed tree node if all its child nodes are being deleted).

Important: Your implementation of GetChildItems must return a collection that implements the INotifyCollectionChanged interface for this to work, e.g. an ObservableCollection<T>. If you return another collection (e.g. a simple list) and ObserveChildItems is true, no exception will be thrown, but a warning will be written to the debug console.

You can explicitly disable collection monitoring by setting the tree's ObserveChildItems dependency property to false.

Explicitly Refreshing the Tree

If the tree can't or shouldn't update itself, you can invoke one of the V-Tree's Refresh methods. This causes the control to completely recreate the underlying tree while optionally preserving its layout (expanded / selected nodes). As a nice side effect, you can also use this feature to apply an arbitrary layout to a tree:

private void CopyTreeLayout(object sender, RoutedEventArgs e)
{
  //get layout from tree A
  TreeLayout layout = CategoryTree.GetTreeLayout();
  //assign layout to tree B

  SynchronizedTree.Refresh(layout);
}

Styling the Tree

V-Tree provides styling point on 3 levels: Bound items, Tree nodes, and the tree itself.

Data Templates

This is the classic WPF way: Rather than styling an UI element (TreeViewItem), you define a DataTemplate for the bound item that is represented by the tree node (in the sample: ShopCategory instances). Just make sure the data template is within the scope of your tree (e.g. declared as a resource of the containing Window) and you're all set. This technique is applied to style the right-hand tree of the sample application:

<Window.Resources>
  <!--
    A data template for bound ShopCategory items:
    Shows a static folder image and the category name
  -->
  <DataTemplate DataType="{x:Type shop:ShopCategory}">
    
    <StackPanel Orientation="Horizontal">

      <Image Source="/Images/WinFolder.gif" />
      <TextBlock Text="{Binding Path=CategoryName}"
                 Margin="2,0,0,0" />

    </StackPanel>
    
  </DataTemplate>
</Window.Resources>

Node Styles

You can explicitly assign a style to all your tree nodes by setting the tree's TreeNodeStyle dependency property. This style will be assigned to every tree node that represents one of your bound item's. Tree nodes are elements of type TreeViewItem, so this is the target type of your style. This technique is applied to style the left-hand tree of the sample application:

<local:CategoryTree TreeNodeStyle="{StaticResource SimpleFolders}" />

<Style x:Key="SimpleFolders"
       TargetType="{x:Type TreeViewItem}">
  <Style.Resources>

    <!-- override default brushes that show ugly background colors -->
    <Brush x:Key="{x:Static SystemColors.HighlightBrushKey}">Transparent</Brush>

    <Brush x:Key="{x:Static SystemColors.ControlBrushKey}">Transparent</Brush>

  </Style.Resources>
  <!-- everything else is done via the data template -->

  <Setter Property="HeaderTemplate"

          Value="{StaticResource CategoryTemplate}" />
</Style>

Note that the style above still uses a data template by setting the HeaderTemplate of the TreeViewItem: The separation between UI and data is still valid, but you have full control over both data representation 'and' your tree node.

Btw: If you want to customize some of your nodes completely differently than others, and doing it in XAML is not an option, you can override the ApplyStyle method and style your nodes individually. This method is always invoked, even if the TreeNodeStyle property is null:

protected override void ApplyNodeStyle(TreeViewItem treeNode, ShopCategory item)
{
  if (IsCheckableCategory(item))
  {
    //render the node with a checkbox

    ApplyCheckBoxStyle(treeNode);
  }
  else

  {
    //just apply the default style
    base(ApplyNodeStyle(treeNode, item);
  }
}

Tree Style

Internally, the V-TreeView operates on a default TreeView control that handles most of the work (no reason to reinvent the wheel here). You can inject your own TreeView instance through the Tree dependency property, but you'll probably just leave it to the control to create a default instance for you. Still, you might want to style the tree, which you can do by setting the V-Tree's TreeStyle dependency property. It takes a style that targets TreeView:

<local:CategoryTree TreeStyle="{StaticResource SimpleTreeStyle}" />

<!-- set the tree's background and border properties -->
<Style x:Key="SimpleTreeStyle" TargetType="{x:Type TreeView}">

  <Setter Property="Background" Value="#AAA" />
  <Setter Property="BorderThickness" Value="4" />

  <Setter Property="BorderBrush" Value="#FFA6AAAB" />
</Style>

Sorting

You can sort your data easily by setting an IEnumerable<SortDescription> to V-Tree's NodeSortDescriptions dependency property. The default value of the property is null, which causes the tree to render items in the order they were received.

<local:CategoryTree NodeSortDescriptions="{StaticResource AscendingNames}" />

The sample application allows you to toggle between two collections which are both declared in XAML. Both sort the tree's ShopCategory items by their CategoryName property in either ascending or descending order. Note that the PropertyName attributes of the declared SortDescriptions contain a Header prefix. This is because sorting happens on the UI layer (TreeViewItem instances), not the underlying items. In order to get to the bound ShopCategory instances, you need to get over the nodes' Header properties:

<!-- sorts categories by names in ascending order -->
<cm:SortDescriptionCollection x:Key="AscendingNames">
  <cm:SortDescription PropertyName="Header.CategoryName"

                      Direction="Ascending" />
</cm:SortDescriptionCollection>

<!-- sorts categories by names in descending order -->

<cm:SortDescriptionCollection x:Key="DescendingNames">

  <cm:SortDescription PropertyName="Header.CategoryName"
                      Direction="Descending" />

</cm:SortDescriptionCollection>

As with styles, there is a virtual method you can override to customize sorting of specific nodes. The example below would skip sorting of the bound root items:

protected override void ApplySorting(TreeViewItem node, ShopCategory item)
{
  //only apply sorting for nested items (keep root item order)

  if (item.ParentCategory != null)
  {
    base.ApplySorting(node, item);
  }
}

Custom Root Nodes

The tree provides a RootNode dependency property (defaults to null) which takes an arbitrary TreeViewItem. You can set this property if you don't want your items to be rendered directly under the tree but a root node that provides some information. Note that this root node does not represent a bound item, and it's not affected by the TreeNodeStyle or NodeContextMenu dependency properties. The screenshot below shows the sample tree with and without a root node:

rootnode-on.png rootnode-off.png

<!-- a custom root node for the tree -->

<TreeViewItem x:Key="CustomRootNode">
  <TreeViewItem.Header>

    ...
  </TreeViewItem.Header>
</TreeViewItem>
    

<local:CategoryTree Items="{Binding Source={StaticResource Shop}, Path=Categories}"
                    RootNode="{StaticResource CustomRootNode}" />

Context Menus

If you want to display a specific context menu on all your nodes, you can set the tree's NodeContextMenu dependency property. This causes the tree to display the menu as soon as one of the tree's nodes is being right-clicked.

<local:CategoryTree NodeContextMenu="{StaticResource CategoryMenu}" />

contextmenu.png

You'll probably use WPF's command system to trigger menu click events. The sample application provides a context menu that allows the user to add new categories or delete existing ones, if they aren't root categories. For the sample, I used the built-in application commands New and Delete.
(In case you're new to WPF commands, here's a good introduction: Commanding Overview)

<Window.Resources>

  <!-- the context menu for the tree -->

  <ContextMenu x:Key="CategoryMenu">

    <MenuItem Header="Add Subcategory"
              Command="New">
      <MenuItem.Icon>

        <Image Source="/Images/Add.png" />

      </MenuItem.Icon>
    </MenuItem>

    <MenuItem Header="Remove Category"

              Command="Delete">

      <MenuItem.Icon>
        <Image Source="/Images/Remove.png" />

      </MenuItem.Icon>

    </MenuItem>
  </ContextMenu>
  
</Window.Resources>

In order to handle commands, MainWindow.xaml (not the tree control!) declares event handlers for both commands:

<Window.CommandBindings>
  <!-- bindings for context menu commands -->
  <CommandBinding Command="New"

                  Executed="AddCategory" />

  <CommandBinding Command="Delete"
                  CanExecute="EvaluateCanDelete"
                  Executed="DeleteCategory" />

</Window.CommandBindings>

And here's the code. The important thing here is GetCommandItem(), which has to take into account that there are two possibilities:

  • The command has been triggered by a clicked menu item of the context menu. However, the right-clicked tree node might not be the one that is currently selected on the tree, so just taking V-Tree's SelectedItem could be wrong. However, if a menu item was clicked, the menu's IsVisible property is true, and V-Tree assigned the clicked node to the menu's PlacementTarget property.
  • The command has been triggered through a shortcut (e.g. by clicking the Delete k key). In this case, the context menu has not been involved at all. The menu's IsVisible property is false, but we can be sure that the command refers to the tree's currently selected item.
/// <summary>/// Determines the item that is the source of a given command.
/// As a command event can be routed from a context menu click

/// or a short-cut, we have to evaluate both possibilities.
/// </summary>

///// <returns></returns>
private ShopCategory GetCommandItem()
{
  //get the processed item

  ContextMenu menu = CategoryTree.NodeContextMenu;
  if (menu.IsVisible)
  {
    //a context menu was clicked

    TreeViewItem treeNode = (TreeViewItem) menu.PlacementTarget;
    return (ShopCategory) treeNode.Header;
  }
  else
  {
    //the context menu is closed - the user has pressed a shortcut

    //-> the command was triggered from the currently selected item

    return CategoryTree.SelectedItem;
  }
}


/// <summary>
/// Creates a sub category for the clicked item

/// and refreshes the tree.
/// </summary>

private void AddCategory(object sender, ExecutedRoutedEventArgs e)
{
  //get the processed item

  ShopCategory parent = GetCommandItem();

  ...

  //mark the event as handled
  e.Handled = true;

}


/// <summary>
/// Checks whether it is allowed to delete a category, which is only

/// allowed for nested categories, but not the root items.
/// </summary>

private void EvaluateCanDelete(object sender, CanExecuteRoutedEventArgs e)
{
  //get the processed item

  ShopCategory item = GetCommandItem();

  e.CanExecute = item.ParentCategory != null;
  e.Handled = true;
}



/// <summary>
/// Deletes the currently processed item. This can be a right-clicked
/// item (context menu) or the currently selected item, if the user

/// pressed delete.
/// </summary>

private void DeleteCategory(object sender, ExecutedRoutedEventArgs e)
{
  //get item

  ShopCategory item = GetCommandItem();

  //remove from parent
  item.ParentCategory.SubCategories.Remove(item);

  //mark the event as handled

  e.Handled = true;
}

Selecting Items before Displaying a Context Menu

If you want the tree to automatically select nodes that are being right-clicked, you can set the SelectNodesOnRightClick dependency property to true. In this case, you never have to evaluate ContextMenu.PlacementTarget as the clicked item is selected in every case.

Updates and Newsletter

This is the first control I've written for WPF, and I'm sure that there will be a few bugs to squash as well as a few nice improvements, both hopefully based on your critical feedback (after all, it's all about learning). So check back for updates and keep the suggestions coming

Future releases of the control will be available here at CodeProject, and I'll also post about them on my blog. If you want to get automatically notified of relevant updates, you can subscribe to a specific newsletter here: http://www.hardcodet.net/newsletter. Just register, then select the control from the list of available newsletters.

I hope you'll find that the control makes a useful addition to your toolbox - have fun!

Changelog

1.0.5 Initial public release (2008.01.29)
1.0.6 Added designer attributes to properties and events, fixed a few article issues. Switched to CPOL (2008.02.08).

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