I recently encountered an interesting challenge: How to display a .NET object’s properties in an expandable tree view? This is something we constantly use inside the Visual Studio debugger:

Visual Studio Debugger’s data tip

The debugger’s data tip control is exactly what I needed. An input would be any .NET object, and the result should be a tree view displaying the object’s properties and fields.

I figured there would be an easy solution I can google and copy-paste, spending no more than 10 minutes on the whole thing. No such luck. Even though I found some similar solutions, they weren’t what I wanted or simply didn’t work.

After some research and coding, spending much more than the intended 10 minutes, I came up with a solution I’m pretty pretty happy with (*self pat on the back).

The general idea

We need to populate a TreeView. To do that, we will need to go over each property of the object and add it to the tree. Then, we will need to go to the children of each of those properties and add them as well. And so on.

As a matter of fact, going over the properties and fields of an object with Reflection is not that easy. You might have virtual properties, abstract classes and generics. Luckily, a library that does exactly that, already exists. You probably know it and use it every day – Yes, it’s Newtonsoft.Json.

Serializing an object to JSON will do exactly what we need – Go over the object’s properties and fields with reflection, turning them into a “tree-like” data structure. Once in JSON format, we can use JavaScriptSerializer (in System.Web.Extensions) to deserialize the JSON and get a data structure we can easily iterate over and populate the tree view.

Thanks to this article (Which showed how to display JSON in a WinForms tree view) for pointing me in the right direction.

By the way, we can use the same solution to visualize a JSON document in a WPF TreeView.

The code

The WPF Control displaying the tree will be:

ObjectInTreeView.xaml:

<UserControl x:Class="TreeViewMagic.ObjectInTreeView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:TreeViewMagic"
			 x:Name="ObjectInTreeViewControl">
	<TreeView ItemsSource="{Binding TreeNodes, ElementName=ObjectInTreeViewControl}">
		<TreeView.Resources>
			<HierarchicalDataTemplate DataType="{x:Type local:TreeNode}" ItemsSource="{Binding Path=Children}">
				<TreeViewItem>
					<TreeViewItem.Header>
						<StackPanel Orientation="Horizontal">
							<TextBlock Text="{Binding Path=Name}"/>
							<TextBlock Text=" : "/>
							<TextBlock Text="{Binding Path=Value}"/>
						</StackPanel>
					</TreeViewItem.Header>
				</TreeViewItem>
			</HierarchicalDataTemplate>
		</TreeView.Resources>
	</TreeView>
</UserControl>

With some code-behind:

public partial class ObjectInTreeView : UserControl
{
    public ObjectInTreeView()
    {
        InitializeComponent();
    }

    public object ObjectToVisualize
    {
        get { return (object)GetValue(ObjectToVisualizeProperty); }
        set { SetValue(ObjectToVisualizeProperty, value); }
    }
    public static readonly DependencyProperty ObjectToVisualizeProperty =
        DependencyProperty.Register("ObjectToVisualize", typeof(object), typeof(ObjectInTreeView), new PropertyMetadata(null, OnObjectChanged));

    private static void OnObjectChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        TreeNode tree = TreeNode.CreateTree(e.NewValue);
        (d as ObjectInTreeView).TreeNodes = new List<TreeNode>() { tree };
    }

    public List<TreeNode> TreeNodes
    {
        get { return (List<TreeNode>)GetValue(TreeNodesProperty); }
        set { SetValue(TreeNodesProperty, value); }
    }
    public static readonly DependencyProperty TreeNodesProperty =
        DependencyProperty.Register("TreeNodes", typeof(List<TreeNode>), typeof(ObjectInTreeView), new PropertyMetadata(null));
}

You’ll notice that whenever the property ObjectToVisualize changes, a new TreeNode is created with the factory method CreateTree.

All the logic of serializing to JSON and deserializing with JavaScriptSerializer happens in 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 = "Root";
        BuildTree(dic, root);
        return root;
    }

    private static void BuildTree(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);
            BuildTree(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);
                BuildTree(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)
            {
                BuildTree(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();
    }
}

Usage and Result

We can use the ObjectInTreeView Control in XAML or in code. In XAML you can write something like:

xmlns:magic="clr-namespace:TreeViewMagic"
...
<magic:ObjectInTreeView ObjectToVisualize="{Binding MyObject}"/>

And in code:

Window x = new Window();
x.Content = new ObjectInTreeView() { ObjectToVisualize = customer };
x.Show();

The result will be:

You might want to style that tree a bit before going to production though.

Hope this helped and happy coding.

M ↓   Markdown
?
Anonymous
0 points
6 years ago

Wow. This is great but I'm having a little trouble adapting it to what I want since I'm really inexperienced in WPF. Is there any way you can post the complete solution?

?
Anonymous
0 points
6 years ago

I managed to build the user control with one issue. Visual Studio complained that InitializeComponent() does not exist so I had to add an empty method called InitializeComponent(). That was strange.

Now I'm trying to add the userControl to a WPF window with a small dictionary of key,values as a test data. I've confirmed that it has the dictionary of values but the user control does not show on the window. Any idea what I might be doing wrong?

?
Anonymous
0 points
6 years ago

Hi Roger,
Unfortunately, I can't find the original code.
If you can wait a bit, I'll recreate it in the following few days.

?
Anonymous
0 points
6 years ago

Hi Michael,

That would be great. I think my problem is related to copping and pasting the xaml over the default xaml Visual Studio gives wieh you start a new project. This post https://social.msdn.microso... seems to indicate that is the problem. I'll keep plugging away on my end until you can post the project.
Thanks again.

?
Anonymous
0 points
6 years ago

Hi. I tried again, this time being careful to not copy paste the XAML and thereby lose the InitializeComponents stuff. Unfortunately it still doesn't work. I've uploaded my solutions here:
https://nofile.io/f/z7km0B8...
https://nofile.io/f/qmK2qtE...

ObjectInTreeView2 is the treeView control and shipTreeViewerWPF is a host with a button and a ObjectInTreeView with the ObjectToVisualize pointing to the button.

Perhaps when you have time you could look at it and see what I'm doing wrong.
Thanks.

?
Anonymous
0 points
6 years ago

One of the links failed to upload. Re-uploading it here:
https://nofile.io/f/ZBrSgCu...

?
Anonymous
0 points
6 years ago

Hi Roger,

Your project was a class library, not a WPF project.
I uploaded a working project here:
https://drive.google.com/op...

?
Anonymous
0 points
6 years ago

Thanks so much for your generous sharing, it's so useful!

?
Anonymous
0 points
6 years ago

Sure, glad it helped!

?
Anonymous
0 points
19 months ago

What a nifty tool. Thank you for sharing! I made a minor mod to display dictionary count properly. ` 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 (value is ICollection)
        {
            var arr = value as ICollection;
            return $"[{arr.Count}]";
        }

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

        return value.ToString();
    }

`