Click here to Skip to main content
15,860,943 members
Articles / Desktop Programming / WPF
Tip/Trick

An Observable Generic Tree Collection

Rate me:
Please Sign up or sign in to vote.
4.58/5 (12 votes)
11 Nov 2015CPOL4 min read 16.9K   1.1K   17  
Simple generic data structure to maintain hierarchical objects

Introduction

In this tip, I will describe a basic Tree structure based on ObservableCollection, where each element may contain child elements of a same type. The number of nested levels is unlimited. This type of collection may become handy if you need to store or manipulate hierarchical data. Since collections are internally based on ObservableCollection class, the tree fits very well for WPF binding scenarios. For example, in the attached sample project, I used it to display data in the TreeView using HierarchicalDataTemplate. Any modification of original object graph gets reflected in the UI automatically, thanks to WPF Data Binding.

Demo App

Another important aspect of this particular implementation of Tree is that my goal was to make the code as compact as possible and rely on the original implementation of ObservableCollection as much as possible. As result, all manipulations with tree, like adding, removing, replacing nodes, etc. are supposed to be performed using well-known methods and properties of standard IList<T>.

Interface

The Tree class implements the following interfaces:

C#
public interface ITree
{
    bool HasItems { get; }
    void Add(object data);
}

public interface ITree<T> : ITree, IEnumerable<ITree<T>>
{
    T Data { get; }
    IList<ITree<T>> Items { get; }
    ITree<T> Parent { get; }
    IEnumerable<ITree<T>> GetParents(bool includingThis);
    IEnumerable<ITree<T>> GetChildren(bool includingThis);
}

ITree is non-generic base interface. The generic interface ITree<T> overrides ITree and exposes additional properties and methods that involve generic type. The reason of splitting the interface to non-generic and generic is to be able to easily identify instances compatible with ITree elements in the Add method.

Note that ITree<T> is also derived from IEnumerable<ITree<T>>. This is an indication that any element of the tree may have its own children. So you can easily iterate over a tree element without having to worry about presence or absence of children in it. It also provides syntactical sugar while using Linq or foreach constructs.

Below are methods and properties of ITree<T>:

  • ?HasItems - returns true if the tree node contains any child elements.
  • Add - adds object(s) to children collection. Note that the method accepts object type as an argument. This is to support scenarios where you can add multiple objects at the same time. I will demonstrate this later.
  • Data - returns generic data object associated with current node.
  • Items - exposes a collection of child elements as IList<T>. You can use this property to add, remove, replace, reset children.
  • GetParents() - a method enumerating all parent nodes, if any. Optionally may include itself in the result.
  • GetChildren() - a method enumerating all child nodes, if any. Optionally may include itself in the result.

Implementation

The Tree class implements the ITree and ITree<T> interfaces described above. In the constructor, you can pass a generic Data object as well as optional child nodes. When child nodes are passed, they are added using the following Add method:

C#
public void Add(object data)
{
    if (data == null)
        return;

    if (data is T)
    { 
        Items.Add(CloneNode(new Tree<T>((T) data)));
        return;
    }

    var t = data as ITree<T>;
    if (t != null)
    {
        Items.Add(CloneTree(t));
        return;
    }

    var o = data as object[];
    if (o != null)
    {
        foreach (var obj in o)
            Add(obj);
        return;
    }

    var e = data as IEnumerable;
    if (e != null && !(data is ITree))
    {
        foreach (var obj in e)
            Add(obj);
        return;
    }

    throw new InvalidOperationException("Cannot add unknown content type.");
}

As you can see, every child element gets cloned before adding to collection. This is necessary to avoid circular references that may lead to unpredictable behavior. As result, if you want to override the Tree class with your own version (for example, you may want to create a class that hides generic type like: class PersonTree : Tree<Person>), you would have to override virtual CloneNode method returning appropriate instance of your type.

Another aspect of Add method is that it can digest any type of IEnumerable or its derivatives as long as their elements are of type ITree<T> or simply T. Here is an example of ITree<string> constructed using various methods:

C#
var tree = Tree.Create("My Soccer Leagues",
    Tree.Create("League A",
        Tree.Create("Division A", 
            "Team 1", 
            "Team 2", 
            "Team 3"),
        Tree.Create("Division B", new List<string> {
            "Team 4", 
            "Team 5", 
            "Team 6"}),
        Tree.Create("Division C", new List<ITree<string>> {
            Tree.Create("Team 7"), 
            Tree.Create("Team 8")})),
    Tree.Create("League B",
        Tree.Create("Division A", 
            new Tree<string>("Team 9"), 
            new Tree<string>("Team 10"), 
            new Tree<string>("Team 11")),
        Tree.Create("Division B", 
            Tree.Create("Team 12"), 
            Tree.Create("Team 13"), 
            Tree.Create("Team 14"))));

Finally, the implementation of Parent property. This property is essential to any hierarchical data structure because it allows you to walk up the tree providing access to the root nodes. By default, the Items collection of a node is not initialized (m_items = null). Once you try to access Items property or add an element to it, the instance of ObservableCollection is created as container for child nodes. The ObservableCollection's CollectionChanged? event is internally hooked to a handler that assigns or unassigns the Parent property of the children being added or removed. Here is how the code looks:

C#
public IList<ITree<T>> Items
{
    get
    {
        if (m_items == null)
        {
            m_items = new ObservableCollection<ITree<T>>();
            m_items.CollectionChanged += ItemsOnCollectionChanged;
        }
        return m_items;
    }
}

private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
    if (args.Action == NotifyCollectionChangedAction.Add && args.NewItems != null)
    {
        foreach (var item in args.NewItems.Cast<Tree<T>>())
        {
            item.Parent = this;
        }
    }
    else if (args.Action != NotifyCollectionChangedAction.Move && args.OldItems != null)
    {
        foreach (var item in args.OldItems.Cast<Tree<T>>())
        {
            item.Parent = null;
            item.ResetOnCollectionChangedEvent();
        }
    }
}

private void ResetOnCollectionChangedEvent()
{
    if (m_items != null)
        m_items.CollectionChanged -= ItemsOnCollectionChanged;
}

Helper Tree Class

The helper static Tree class provides syntactical sugar for creating tree nodes. For example, instead of writing:

C#
var tree = new Tree<Person>(new Person("Root"),
    new Tree<Person>(new Person("Child #1")),
    new Tree<Person>(new Person("Child #2")),
    new Tree<Person>(new Person("Child #3")));

You could express the same code in a slightly cleaner way:

C#
var tree = Tree.Create(new Person("Root"),
    Tree.Create(new Person("Child #1")),
    Tree.Create(new Person("Child #2")),
    Tree.Create(new Person("Child #3")));

Also, there is a helper Visit method. It takes a tree node ITree<T> and an Action<ITree<T>> recursively traversing the tree node and calling the action for each node:

C#
public static void Visit<T>(ITree<T> tree, Action<ITree<T>> action)
{
    action(tree);
    if (tree.HasItems)
        foreach (var item in tree)
            Visit(item, action);
}

For example, you can use this method to print a tree:

C#
public static string DumpElement(ITree<string> element)
{
    var sb = new StringBuilder();
    var offset = element.GetParents(false).Count();
    Tree.Visit(element, x =>
    {
        sb.Append(new string(' ', x.GetParents(false).Count() - offset));
        sb.AppendLine(x.ToString());
    });
    return sb.ToString();
}

Summary

The Tree<T> collection is a simple hierarchical data structure with the following features:

  • Supports generic data types
  • Automatically maintains a reference to Parent node
  • Friendly to WPF binding
  • Fluid syntax of constructing and accessing elements
  • Utilizes standard .NET IList<T> features and functionality

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --