The Tree View Control

 

 

The Tree View control is ideal for displaying hierarchical data. While there is a great deal you can do with this control, in its simplest form it is fairly straightforward to work with, once you grok the essential recursiveness it demands (well, requests strongly).

We’ll look at two examples: the TreeView raw, and then using DataBinding with a tiny bit of customization. Here’s what we’re building:

— Top of Streaming Application —

— End of Streaming Application —

Everything that follows is based on the work of Stephen R Strong who contributed his efforts in support of this blog, and whose work is very much appreciated.

To create the application shown above, we need to a hierarchical data source and we need a pair of treeview controls.

The easiest way to obtain the data is to just create it in memory (though of course, your source might be an xml file, retrieved from a database, etc.).

Hierarchical Data

The Data class I created for this example is defined like this:

public class Data
{
   public string Name { get; set; }
   public Data Parent { get; set; }
   public List<Data> Children;
   public int Depth  { get { return Parent == null ? 0 : Parent.Depth +1; } }
   

I opted to provide a default constructor, though it does so little, I could have let the compiler create one for me. The CreateChild method adds a new child to the current Data object.

public Data()
{
   Name=string.Empty;
   Parent = null;
   Children = new List<Data>();
}

public Data CreateChild( string name )
{
   Data child = new Data() { Name = name, Parent = this};
   Children.Add(child);
   return child;
}

Finally, the Data class provides a static method that returns a hierarchical collection of information about the United States,

public static Data CreateDataSample()
{
   Data root = new Data() { Name = "United States" };
   Data states = root.CreateChild( "States" );
   states.CreateChild( "Alabama" ).CreateChild( "Montgomery" );
   states.CreateChild( "Alaska" ).CreateChild( "Juneau" );
   states.CreateChild( "Arizona" ).CreateChild( "Phoenix" );
   states.CreateChild( "Arkansas" ).CreateChild( "Little Rock" );
   Data presidents = root.CreateChild( "Presidents" );
   Data W = presidents.CreateChild( "Washington" );
   Data WTerm = W.CreateChild( "Took office" );
   WTerm.CreateChild( "1789" );
   WTerm.CreateChild( "1793" );
   Data A = presidents.CreateChild( "Adams" ).CreateChild( "Took office" ).CreateChild( "1797" );
   Data J = presidents.CreateChild( "Jefferson" );
   Data JTerm = J.CreateChild( "Took office" );
   JTerm.CreateChild( "1801" );
   JTerm.CreateChild( "1805" );
   return root;
}

Note that CreateChild returns a Data object, and we take advantage of that in the third line where we create a Child of states whose Name field is Alabama, and then give that Data object a child whose name field is Montgomery (the capital of Alabama).

Ultimately, we return just the root Data object; but the structure we’ve created looks like this:

USOutline 

Here’s the complete Xaml for the page that displays this data,

<UserControl x:Class="TreeView.TreeViewPage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:control="clr-namespace:Microsoft.Windows.Controls;
assembly=Microsoft.Windows.Controls" Width="600" Height="600"> <Grid x:Name="LayoutRoot" Background="White"> <control:TreeView x:Name="LeftTreeView" HorizontalAlignment="Left" Background="Wheat" /> <control:TreeView x:Name="RightTreeView" HorizontalAlignment="Right" Background="Beige" /> </Grid> </UserControl>

The interesting work, of course, is done in the code behind.  L

public partial class TreeViewPage : UserControl
{
   public TreeViewPage()
   {
      InitializeComponent();
      Data root = Data.CreateDataSample();
      FillTree( LeftTreeView.Items, root );
      FillBoundTree( RightTreeView.Items, root );
   }

Recursion

After the call to InitializeComponent we call our static method that returns the root node (United States) which is pregnant with the entire data structure. We pass two arguments to a private helper method named FillTree:

  1. The Items Collection of the TreeView control declared in the Xaml
  2. The Data instance that serves as the root of the tree

The helper method is created to provide a place for recursion, because the process of adding nodes to the tree is (almost) inherently recursive.

I wrote almost inherently recursive because of course you can do this without recursion, even without data binding, but there are some things that just lend themselves to recursion and this is surely one.
 private void FillTree( ItemCollection itemColl, Data dataNode )
 {
    TreeViewItem tvi = new TreeViewItem();
    itemColl.Add( tvi );
    tvi.Header = dataNode.Name;
    //….

FillTree creates an instance of a TreeViewItem (a node in a TreeView) and adds it to the collection that is passed in (in this first case, to the items collection of the TreeView control).

It then sets the tree view item’s header property to the Name property of the Data object it received.  At this point we can conceive of things as looking like this:

TreeViewFirstStep

It is important to note that the TreeViewItem does not hold a copy of the Data object, but rather the Header property of the TreeViewItem gets its string from the DataObject.

The FillTree method then iterates through each child in the current Data object’s Children property (which you will remember is a List<Data>.

    foreach ( Data childDataNode in dataNode.Children )
    {
       FillTree( tvi.Items, childDataNode ); // dang. dang. (curse, recurse)
    }
 }  // end method

For each child it finds, it recurses, passing in the Items collection of the new TreeView Item and, one by one, the children of the current Data object,

Recurse

Thus, the first time the method recurses, the parameters are now the Items collection of the TreeViewItem whose header is United States, and the Data object States.

In this way each data object’s name is added to a TreeViewItem in the appropriate “level” of the tree.

Final line of the constructor of TreeViewPage calls FillBoundTree

public TreeViewPage()
{
   InitializeComponent();
   Data root = Data.CreateDataSample();
   FillTree( LeftTreeView.Items, root );
   FillBoundTree( RightTreeView.Items, root );
}

 

Data Binding

This second method looks a lot like FillTree but works quite differently.

private void FillBoundTree( ItemCollection itemColl, Data dataNode )
{
   TreeViewItem tvi = new TreeViewItem();
   itemColl.Add( tvi );
   tvi.DataContext = dataNode;
   tvi.Header = new TreeViewNode();
   foreach ( Data childDataNode in dataNode.Children )
   {
      FillBoundTree( tvi.Items, childDataNode ); 
   }
}

The key is found in the two lines I’ve shown in red.  In this case, rather than copying the text of the Name field to the Header, we set the DataContext (which signals right away that we’re doing some kind of data binding) and we set the Header to an instance of a TreeViewNode.

TreeViewNode is a user control. Here is the complete Xaml for TreeViewNode,

<UserControl x:Class="TreeView.TreeViewNode"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="300" >
    <Grid x:Name="LayoutRoot" Background="Bisque">
    <Border BorderBrush="Black"
            BorderThickness="4">
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Name}"
                   Margin="3" />
        <TextBlock Text="{Binding Depth}"
                   Margin="3"/>
      </StackPanel>
    </Border>
  </Grid>
</UserControl>

(No work is done in TreeViewNode.cs)

You can see that the heart of this user control is a pair of TextBlock objects, one of which is bound to the Name property and the other to the Depth property of the Data object. As each Data object is passed in to the FillBoundTree method it serves as the DataContext from which these properties are extracted.


This work is licensed under a Creative Commons Attribution By license.

About Jesse Liberty

Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. His latest book, Building APIs with .NET will be released early in 2025. Liberty is a Senior SW Engineer for CNH and he was a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a Microsoft MVP.
This entry was posted in z Silverlight Archives and tagged . Bookmark the permalink.

6 Responses to The Tree View Control

Comments are closed.