Bind Object to WPF TreeView

11,575

Solution 1

Ok this is where the HierarchicalDataTemplate will save you. The trick is you will need to use two different hierarchical templates, since you have a three-level hierarchy here. I have constructed a simple UserControl to illustrate. First, here is some code-behind creating model data similar to what you have:

public partial class ThreeLevelTreeView : UserControl
{
    public ArrayList DeviceGroups { get; private set; }

    public ThreeLevelTreeView()
    {
        DeviceInstance inst1 = new DeviceInstance() { Name = "Instance1" };
        DeviceInstance inst2 = new DeviceInstance() { Name = "Instance2" };
        DeviceInstance inst3 = new DeviceInstance() { Name = "Instance3" };
        DeviceInstance inst4 = new DeviceInstance() { Name = "Instance4" };

        DeviceType type1 = new DeviceType() { Name = "Type1", DeviceInstances = new ArrayList() { inst1, inst2 } };
        DeviceType type2 = new DeviceType() { Name = "Type2", DeviceInstances = new ArrayList() { inst3 } };
        DeviceType type3 = new DeviceType() { Name = "Type3", DeviceInstances = new ArrayList() { inst4 } };
        DeviceType type4 = new DeviceType() { Name = "Type4" };

        DeviceGroup group1 = new DeviceGroup() { Name = "Group1", DeviceTypes = new ArrayList() { type1, type2 } };
        DeviceGroup group2 = new DeviceGroup() { Name = "Group2", DeviceTypes = new ArrayList() { type3, type4 } };

        DeviceGroups = new ArrayList() { group1, group2 };

        InitializeComponent();
    }
}

public class DeviceGroup
{
    public string Name { get; set; }
    public ArrayList DeviceTypes { get; set; }
}

public class DeviceType
{
    public string Name { get; set; }
    public ArrayList DeviceInstances { get; set; }
}

public class DeviceInstance
{
    public string Name { get; set; }
}

Nothing difficult here, but note that you should use ObservableCollection instead of ArrayList if you want to add and remove from your collections dynamically. Now let's look at the XAML for this control:

<UserControl x:Class="TestWpfApplication.ThreeLevelTreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestWpfApplication"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<TreeView ItemsSource="{Binding DeviceGroups}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding DeviceTypes}">
            <HierarchicalDataTemplate.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding DeviceInstances}">
                    <TextBlock Text="{Binding Name}"/>
                </HierarchicalDataTemplate>
            </HierarchicalDataTemplate.ItemTemplate>
            <TextBlock Text="{Binding Name}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

And here is the result:

alt text http://img684.imageshack.us/img684/6281/threeleveltreeview.png

Solution 2

I lately had to deal with a similar issue and after much research was able to get to a good generic solution. My problem was a bit more generic: Visualize a .NET object's properties in a tree view. So given this class ```

class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public List<Person> Children { get; set; }
}

I should see a tree like for an example instance:

- root
  - FirstName: John
  - LastName: Smith
  - Children:
    - [0]
      - FirstName: Ann
      - LastName: Smith

``` It's rather difficult to use reflection and go over an object's properties and such. Luckily, we already have a library that does that - Newtonsoft.Json. My trick is to serialize the object with Newtonsoft.Json, then deserialize with System.Web.Script.Serialization.JavaScriptSerializer.

JavaScriptSerializer returns ArrayLists and Dictionaries, which are pretty simple to go over and add to the tree.

Thanks to this article which gave me some of the concept ideas.

Anyway, here's the entire code:

In XAML:

<TreeView ItemsSource="{Binding TreeItemsSource}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:TreeNode}" ItemsSource="{Binding Path=Children}">
            <TreeViewItem>
                <TreeViewItem.Header>
                    <StackPanel Orientation="Horizontal" Margin="-10,0,0,0">
                        <TextBlock Text="{Binding Path=Name}"/>
                        <TextBlock Text=" : "/>
                        <TextBox Text="{Binding Path=Value}" IsReadOnly="True"/>
                    </StackPanel>
                </TreeViewItem.Header>
            </TreeViewItem>
        </HierarchicalDataTemplate>
    </TreeView.Resources>
</TreeView>

In ViewModel:

public IEnumerable<TreeNode> TreeItemsSource
{
    get
    {
        TreeNode tree = TreeNode.CreateTree(SelectedSession);
        return new List<TreeNode>() { tree };
    }
}

And the TreeNode class

public class TreeNode
{
    public string Name { get; set; }
    public string Value { get; set; }
    public List<TreeNode> Children { get; set; } = new List<TreeNode>();

    public static TreeNode CreateTree(object obj)
    {
        JavaScriptSerializer jss = new JavaScriptSerializer();
        var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(obj);
        Dictionary<string, object> dic = jss.Deserialize<Dictionary<string, object>>(serialized);
        var root = new TreeNode();
        root.Name = "session";
        BuildTree2(dic, root);
        return root;
    }

    private static void BuildTree2(object item, TreeNode node)
    {
        if (item is KeyValuePair<string, object>)
        {
            KeyValuePair<string, object> kv = (KeyValuePair<string, object>)item;
            TreeNode keyValueNode = new TreeNode();
            keyValueNode.Name = kv.Key;
            keyValueNode.Value = GetValueAsString(kv.Value);
            node.Children.Add(keyValueNode);
            BuildTree2(kv.Value, keyValueNode);
        }
        else if (item is ArrayList)
        {
            ArrayList list = (ArrayList)item;
            int index = 0;
            foreach (object value in list)
            {
                TreeNode arrayItem = new TreeNode();
                arrayItem.Name = $"[{index}]";
                arrayItem.Value = "";
                node.Children.Add(arrayItem);
                BuildTree2(value, arrayItem);
                index++;
            }
        }
        else if (item is Dictionary<string, object>)
        {
            Dictionary<string, object> dictionary = (Dictionary<string, object>)item;
            foreach (KeyValuePair<string, object> d in dictionary)
            {
                BuildTree2(d, node);
            }
        }
    }

    private static string GetValueAsString(object value)
    {
        if (value == null)
            return "null";
        var type = value.GetType();
        if (type.IsArray)
        {
            return "[]";
        }

        if (value is ArrayList)
        {
            var arr = value as ArrayList;
            return $"[{arr.Count}]";
        }

        if (type.IsGenericType)
        {
            return "{}";
        }

        return value.ToString();
    }

}
Share:
11,575
Robert
Author by

Robert

Updated on June 04, 2022

Comments

  • Robert
    Robert almost 2 years

    I would like to know how to bind a custom data type to a TreeView.

    The data type is basically an arraylist of objects that contain other arraylists. Access would look something like this:

    foreach (DeviceGroup dg in system.deviceGroups)
        {
            foreach (DeviceType dt in dg.deviceTypes)
            {
                foreach (DeviceInstance di in dt.deviceInstances)
                {
    
                }
            }
        }
    

    I would like the TreeView to look something like this:

    DeviceGroup1

     --> DeviceType1
          --DeviceInstance1
          --DeviceInstance2
     --> DeviceType2
          --DeviceInstance1
    

    DeviceGroup2

     --> DeviceType1
          --DeviceInstance1
     --> DeviceType2
    
  • Robert
    Robert about 14 years
    Thank you so much for your response. Your example works great when the class is contained within ThreeLevelTreeView, but I'm having some difficulty understanding how to bind an external class to the datacontext of the ThreeLevelTreeView. How would I go about doing that?
  • Charlie
    Charlie about 14 years
    You don't have to bind the DataContext. You could simply set it in code like this: yourTreeView.DataContext = yourClass.
  • Robert
    Robert about 14 years
    I was wondering how I would bind data to the treeview without using the user control. I have to reroute all the event handlers through the user control and I find that kind of tedious. I have copied out the TreeView part of the user control and placed it in my MainWindow.xaml. I bind the data in the Window_Loaded event like this: _systemTreeView.DataContext = DeviceGroups; But nothing appears in the TreeView.
  • Charlie
    Charlie about 14 years
    Switch from using ArrayList to ObservableCollection. ArrayList does not support automatic change notifications so your TreeView doesn't know any items are being added. This worked in my example because I loaded the items and set the DataContext before InitializeComponent was called in the constructor. Since you are binding in the Loaded event the TreeView is never getting the notification. But ObservableCollection supports automatic change notifications so any items you add will update the binding.
  • Robert
    Robert about 14 years
    I have switched to ObservableCollection, but it still does not work. Adding/Removing objects changes dynamically which is great, but when I try to add a brand new datacontext, the whole treeview disappears. I think it may have something to do with how the binding works in the xaml for ThreeLevelTreeView, but I'm not sure.
  • Robert
    Robert about 14 years
    I guess what I'm really asking is can you do this without making it a User Control?
  • Charlie
    Charlie about 14 years
    Yes of course. If you wanted to put it in a Window you could. Just make sure the Window's DataContext is set to itself and the top level items collection is located on the Window. Either way it will be no different than nesting the UserControl inside the Window, just less encapsulated.
  • Robert
    Robert almost 14 years
    How do I change the text color of the instance names to red?
  • Charlie
    Charlie almost 14 years
    Change the innermost data-template's TextBlock to this: <TextBlock Text="{Binding Name}" Foreground="Red"/>