WPF - Selecting TreeViewItems in code

|

This appears to be a common problem for people with various solutions, most involving some walking of the visual tree, which to my mind is a little overkill. Also, you've got to wonder what the WPF team were thinking when they left out such a core (and apparently much demanded) feature, so there has to be an easier way, right? [...snip...]

As with most things like this you rarely find a complete solution in a single location, but that is what this post (and possibly a one or two more) will give you - which will not involve walking the visual tree, but will actually leverage the WPF data binding engine.

Let us start with the application scenario, to give us something to work with. Consider the following XML:

<?xml version="1.0" encoding="utf-8" ?>
<Library>
    <Author Name="Edward Tufte">
        <Book Name="The Visual Display of ..." />
        <Book Name="Envisioning Information" />
        <Book Name="Visual Explanations" />
        <Book Name="Beautiful Evidence" />
    </Author>
    <Author Name="Chris Sells">
        <Book Name="Windows Forms Programming in C#" />
        <Book Name="Windows Forms 2.0 Programming" />
        <Book Name="Programming WPF" />
    </Author>
    <Author Name="Charles Petzold">
        <Book Name="Programming Microsoft Windows with C#" />
        <Book Name="Application = Code + Markup" />
        <Book Name="3D Programming for Windows" />
    </Author>
</Library>

A very simple schema for a library of books. Our first step is to get this data into a TreeView. We'll use CLR objects, rather than XML directly (the reasoning for this will be apparent later) - so lets assume that there is an appropriate object model of CLR objects to support this data schema, something like:

LibraryDataModel

To wire a TreeView and this object model together is pretty straight forward: we provide the TreeView with a HierarchicalDataTemplate, something like this:

<TreeView x:Name="tree">
  <TreeView.Resources>
     <HierarchicalDataTemplate 
       DataType="{x:Type ld:Author}"
       ItemsSource="{Binding Path=Books}">
       <TextBlock Text="{Binding Name}" />
     </HierarchicalDataTemplate>
     <DataTemplate DataType="{x:Type ld:Book}">
       <TextBlock Text="{Binding Title}" />
     </DataTemplate>
  </TreeView.Resources>
</TreeView>
  

The only significant difference between a DataTemplate and a HierarchicalDataTemplate is the ItemsSource property; this simply tells the TreeView where to get the children for the templated item from, the Books property in this example.

Next we must provide the TreeView with its ItemsSource, which is the list of Authors; some like this:

private void OnLoaded(object sender, RoutedEventArgs e)
{
   this.tree.ItemsSource = DataProvider.GetAuthors();
}

This will produce an output similar to the following:

LibraryTreeViewOutput 

Right, so that's the basic application, lets start trying to tackle the actual problem.

Before we go too much further we need to talk a little bit about what makes up a TreeView. The important thing to understand for this solution is that each tree node is actually a type called TreeViewItem, and it is this item that displays the data template we defined earlier for our custom types Author and Book.

TreeViewExplained

So you should be able to see from the image above (click on it to see a lager version) is that the TreeViewItem is the container for our content. The TreeView kindly provides a simple way for us to work with the TreeViewItem indirectly by using the TreeView.ItemsContainerStyle. This property enables us to modify the style associated with the TreeViewItem template - remember that a Style tweaks the properties on UI elements that the Template loaded in the visual tree to render our view. Simply put: a Style work with objects already in the visual tree by changing the values of (dependency) properties.

What this means for us is that the TreeViewItem has two properties that we can use: IsSelected and IsExpanded, if we bind our business objects to these two properties via the ItemsContainerStyle like so:

<TreeView x:Name="tree">
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected"
                    Value="{Binding IsSelected}"
/>
            <Setter Property="IsExpanded"
                    Value="{Binding IsExpanded}"
/>
        </Style>
    </TreeView.ItemContainerStyle>

We can effect the behaviour of the tree buy manipulating our business objects! To make this all work we need to add these two properties to all the business objects that can appear in the tree control, Author and Book in this case. Then we must add change notification for the binding system to notice when we update these properties, which will then in turn update the tree control. Here's the new data model:

LibraryDataModelWithChangeNotification

With all this in place, we're all set to manipulate the tree by using our data model. For example, when the application loads, the tree nodes are all collapsed (IsExpanded defaults to false), therefore to expand all the Author nodes and to select the Book with the title "Beautiful Evidence" we would write the following code:

foreach (Author a in this.tree.ItemsSource)
{
    a.IsExpanded = true;

    if (a.Name == "Edward Tufte")
    {
        foreach (Book b in a.Books)
        {
            if (b.Title == "Beautiful Evidence")
            {
                b.IsSelected = true;
                break;
            }
        }
    }
}

Running this code should produce something that looks like this:

LibraryTreeViewRunning

Here's the complete code for this simple example, note that this code is just the bare bones of what we've been talking about in this post, it's far from production code:

PaulJ.TreeViewManipulation.zip

This is not the end of the story, however. This solution is still a little sucky, due to the tight coupling between the data model and the requirements of the UI by way of the TreeViewItem; the current data model would not be great for reuse in a WCF service or a Silverlight application for example.

Therefore in future post we'll look at tackling the separation of concerns with a couple of design patterns, probably the Decorator pattern (regular readers would have heard that story before) and the most likely sound solution, in my opinion, will probably be a Model View Controller approach, but we'll see - we can figure it out together.

Until next time, enjoy!

kick it on DotNetKicks.com

No comments: