Thursday, February 7, 2008

TreeNode - a class to help bind to a TreeView

filledtree

I made a very simple mistake binding to a treeview that took me forever to figure out what was going wrong. 

I realized that most the samples of binding to a treeview miss the core case of binding to a tree of objects with a single root.  (I'm sure there's a good pun here about seeing the forest for the trees).

Let's consider a class called TreeNode that's structured like so:

public class TreeNode {
  public object Value { get; set; }
  public ObservableCollection<TreeNode> Children { get; }
}

Assume your tree is defined in code like so:
  // Window.xaml.cs constructor
  TreeNode tree = new TreeNode("root",
        new TreeNode("Child 1",
                new TreeNode("Child 1.1"),
                new TreeNode("Child 1.2")),
        new TreeNode("Child 2",
                new TreeNode("Child 2.1"),
                new TreeNode("Child 2.2")));

We want the TreeView itself to create a TreeViewItem for the Root, and then the subsequent TreeViewItems to generate TreeViewItems for the Root.Children and the Root.Children.Children ... and so on.

Lets break it down into two issues: showing the Root and showing the Children.

 

Step 1: Showing the root.  roottreeitem

Like any other items control, you use the TreeView.ItemsSource to databind to the Root object.

Unfortunately, our object Is not a list, so it doesn't work with ItemsSource.  If you bind an object that is not IEnumerable to the ItemsSource property, it will silently fail. This is easily solved one of two ways - create a one element list that contains the root object or implement IEnumerable on TreeNode.

 

IEnumerable solution - Option A: Assign the ItemsSource a one element array containing our Root tree node object

  // Window.xaml.cs constructor
  this.DataContext = new TreeNode[] { tree };

<Window ...>
    <Grid>
       <!-- ommiting Source on the Binding means 
            pick up the data from the DataContext
            property of the Grid or Window -->

       <TreeView Name="_treeView"
            
ItemsSource="{Binding}"/>
    </Grid>
</Window>

IEnumerable solution - Option B: Implement IEnumerable on our TreeNode class so that it can be directly assigned to TreeView.ItemsSource.

public class TreeNode : IEnumerable {
   public IEnumerator GetEnumerator() {
        TreeNode[] treeNode =
                 
new TreeNode[] { this };
        return treeNode.GetEnumerator();
    }

}
  // Window.xaml.cs constructor
  this.DataContext =  tree;

<Window ...>
    <Grid>
       <TreeView Name="_treeView" 
             
ItemsSource="{Binding}"/>
    </Grid>filledtree
</Window>

Step 2: Showing the children.

If you're at this point you should see the picture in step one with just the root displayed.  Now it's time to show the children. 

Using a HierarchicalDataTemplate, you can say:

  • "whenever you see a TreeNode" 
  • "use a TextBlock to display the TreeNode.Value of the current item"
  • "for the next level down, display items in the TreeNode.Children collection"

In technicolor XAML, this would be:

<HierarchicalDataTemplate
    DataType="{x:Type local:TreeNode}" 
    ItemsSource="{Binding Path=Children}">
       
<TextBlock Text="{Binding Path=Value}"/>
 
</HierarchicalDataTemplate>

You can either directly assign this to the TreeView.ItemTemplate property, or put this in a ResourceDictionary in a parent element of the TreeView.

Code for TreeNode.

 

Window.xaml


<Window x:Class="WpfApplication1.Window1"
   xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="Window1" Height="300" Width="300">
    <Grid>
        <TreeView Name="_treeView"  ItemsSource="{Binding}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate
                     DataType="{x:Type local:TreeNode}"
                     ItemsSource="{Binding Path=Children}">
                    <TextBlock Text="{Binding Path=Value}"/>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>

Window.xaml.cs

   public partial class Window1 : Window {
        public Window1() {
            InitializeComponent();

            TreeNode tree = new TreeNode("root",
                new TreeNode("Child 1",
                        new TreeNode("Child 1.1"),
                        new TreeNode("Child 1.2")),
                new TreeNode("Child 2",
                        new TreeNode("Child 2.1"),
                        new TreeNode("Child 2.2")));

            this.DataContext  = tree;

           // alternately, you can directly assign
           // _treeView.ItemsSource = tree;

        }
    }

 

No comments: