Click here to Skip to main content
Email Password   helpLost your password?

Contents

Introduction

I wrote a tree collection for .NET 1.1 last year - A Tree Collection [^]. This has proved to be quite popular, so I decided to write one for .NET 2.0 using generics. I reused a lot of code, but I had to write a lot of new interfaces from scratch, especially the enumerators. Once again, I don't know why MS didn't include an implementation of such a basic collection in the framework. This implementation fills that gap and is useable straight out of the box.

I recommend reading the Structure section first, but if you just want to use this code, then look at the Quickstart section.

Structure

This code started life as a composite pattern. However, I soon realized that I didn't need two classes as a node is a tree, and a tree is a node. So I rolled them into one class called NodeTree. This has probably been done before, but it just seemed like a good idea to me.

Interfaces

Although only one class, NodeTree, is being used to model the tree, the consumer does not directly use it. They manipulate the collection through two interfaces: INode and ITree. The NodeTree class derives from both of these interfaces:

partial class NodeTree<T> : ITree<T>, INode<T>

So, a NodeTree is the internal representation of both a node and a tree. However the two interfaces only declare members that make sense to that particular point of view. There is considerable equivalent behaviour between the two interfaces; for example, they both declare InsertChild methods. In general, the INode interface is the larger, although ITree does declare some unique members like Clear.

Data

The purpose of a collection is to hold data. In this collection, the data is held in a private field, _Data, in the NodeTree class. Access to this object is through the INode interface, which declares a property Data:

partial interface INode<T>
{
    T Data { get; set; }
}

ITree does not declare this property, as it does not make sense, as we shall see later.

Node structure

Each node has the following structure:

Figure 1: Node structure

Tree structure

Nodes link together to form a tree with the following structure:

Figure 2: Tree structure

Terminology

The Root node defines a tree. You cannot use it to store data - it is just a place-holder. The Top node(s) defines the user part of the tree. You can have multiple Top nodes, but you don't have to. You are responsible for building your tree to match your requirements. A branch is the collection of nodes which are the Children of a common Parent and are connected by the Previous and Next properties. The node at the beginning of a branch is called a First node. Similarly, the node at the end of a branch is called a Last node. The Child node of a parent is the First node in its branch.

Quickstart

This section will get you going in the shortest possible time. The examples show a tree with a data type of Element.

Firstly, you must create your tree:

ITree<Element> tree = NodeTree<Element>.NewTree();

Next, create a top node:

INode<Element> top = tree.AddChild( new Element() );

Note that you add instances of your data type and a node is created and inserted into the tree for you.

Then add leaf nodes:

INode<Element> leaf1 = top.AddChild(new Element());
INode<Element> leaf2 = top.AddChild( new Element() );

You can now iterate over your tree:

foreach( INode<Element> node in tree.All.Nodes )
    DoSomething( node.Data );

or:

foreach( Element element in tree.All.Values )
    DoSomething( element );

Documentation

This section details the capabilities of the collection.

Instantiation

These static methods create new trees:

partial class NodeTree<T>
{
    public static ITree<T> NewTree() { ... }
    public static ITree<T> 
       NewTree(IEqualityComparer<T> dataComparer) { ... }
}

The first method creates a new tree with the default DataComparer, using EqualityComparer<T>.Default. If T implements IEquatable, then it uses that implementation. Otherwise, it uses the Equals method.

The second method allows you to specify the IEqualityComparer<T> to use.

Conversion

Each interface has a property that allows conversion to the other:

partial interface ITree<T>
{
    INode<T> Root { get; }
}

partial interface INode<T>
{
    ITree<T> Tree { get; }
}

Counts

Various metrics about a tree or node are available:

partial interface ITree<T>
{
    int Count { get; }
    int DirectChildCount { get; }
}

partial interface INode<T>
{
    int Count { get; }
    int DirectChildCount { get; }

    int Depth { get; }
    int BranchIndex { get; }
    int BranchCount { get; }
}

Count returns the number of nodes below the current node + 1 for the current node. The root node is not counted. DirectChildCount returns just the number of direct children of the current node. The Depth of a node is the number of parents it has, not including the root node. Thus, the depth of a top node is 0 and the depth of a top node's child is 1, etc. A branch is a collection of sibling nodes. BranchCount is the number of sibling nodes and BranchIndex is the zero-based index of the current node in its branch.

Relationships

These properties provide access to other nodes in a tree:

partial interface ITree<T>
{
}

partial interface INode<T>
{
    INode<T> Parent { get; }
    INode<T> Previous { get; }
    INode<T> Next { get; }
    INode<T> Child { get; }

    INode<T> Root { get; }
    INode<T> Top { get; }
    INode<T> First { get; }
    INode<T> Last { get; }

    INode<T> LastChild { get; }
}

The Parent, Previous, Next and Child properties allow you to navigate through the immediate relations of a node. More distant relations can be accessed through the Root, Top, First, Last and LastChild properties of a node.

Boolean properties

These properties provide information about the relations of a node:

partial interface ITree<T>
{
}

partial interface INode<T>
{
    bool IsTree { get; }
    bool IsRoot { get; }

    bool IsTop { get; }
    bool IsFirst { get; }
    bool IsLast { get; }

    bool HasParent { get; }
    bool HasPrevious { get; }
    bool HasNext { get; }
    bool HasChild { get; }
}

The IsTree property is only true for a root node at the base of a tree. The IsRoot property is true for any node that has no parent. This should only be true for a root node at the base of a tree, as nodes cannot exist outside a tree. The IsTop, IsFirst and IsLast properties provide information about the position of a node within a tree. The HasParent, HasPrevious, HasNext and HasChild properties provide information about the immediate relations of a node.

Adding an element

These methods allow you to populate your tree:

partial interface ITree<T>
{
    INode<T> InsertChild( T o );

    INode<T> AddChild( T o );
}

partial interface INode<T>
{
    INode<T> InsertPrevious( T o );
    INode<T> InsertNext( T o );
    INode<T> InsertChild( T o );

    INode<T> Add( T o );
    INode<T> AddChild( T o );
}

These methods wrap the given element in a new node and insert or add this node in the tree. The InsertChild methods insert a node at the beginning of the child branch and the AddChild methods add a node to the end of the child branch. The Add method adds a node to the end of the current branch.

Adding a tree

These methods work with complete trees:

partial interface ITree<T>
{
    void InsertChild( ITree<T> tree );

    void AddChild( ITree<T> tree );
}

partial interface INode<T>
{
    void InsertPrevious( ITree<T> tree );
    void InsertNext( ITree<T> tree );
    void InsertChild( ITree<T> tree );

    void Add( ITree<T> tree );
    void AddChild( ITree<T> tree );
}

These methods work in the same way as adding an element, but operate on complete trees.

Moving a node

These methods allow you to move nodes around in your tree:

partial interface ITree<T>
{
}

partial interface INode<T>
{
    bool CanMoveToParent { get; }
    bool CanMoveToPrevious { get; }
    bool CanMoveToNext { get; }
    bool CanMoveToChild { get; }
    bool CanMoveToFirst { get; }
    bool CanMoveToLast { get; }

    void MoveToParent();
    void MoveToPrevious();
    void MoveToNext();
    void MoveToChild();
    void MoveToFirst();
    void MoveToLast();
}

The Can properties indicate whether a particular operation is possible. The methods actually do the work of moving a node (and its children along with it).

Copying

There are two ways to copy the sub-tree defined by a NodeTree: Copy and DeepCopy. Copy creates new tree nodes, but sets the data property of each new node to reference the same instance of the data as the original node. DeepCopy attempts to make a copy of the data as well. I have defined an interface IDeepCopy as:

public interface IDeepCopy
{
    object CreateDeepCopy();
}

If the data object supports this interface, then CreateDeepCopy is called on the data from each node being copied. If the data does not support this interface, then ICloneable is tried. If this interface is also not implemented, an attempt is made to instantiate a new object of the same type as the data object, using the copy constructor through reflection. If no copy constructor exists, then DeepCopy gives up and just copies a reference to the data.

Manipulating sub-trees

These methods allow you to create a new tree from a node and its children:

partial interface ITree<T>
{
    ITree<T> Copy();
    ITree<T> DeepCopy();
}

partial interface INode<T>
{
    ITree<T> Cut();
    ITree<T> Copy();
    ITree<T> DeepCopy();
    void Remove();
}

These methods operate on a node to produce a tree.

Working with elements

These methods find a node that contains the specified element and then operate on that node:

partial interface ITree<T>
{
    INode<T> this[ T item ] { get; }

    bool Contains( INode<T> node );
    bool Contains( T item );

    ITree<T> Cut( T item );
    ITree<T> Copy( T item );
    ITree<T> DeepCopy( T item );
    bool Remove( T item );
}

partial interface INode<T>
{
    INode<T> this[ T item ] { get; }

    bool Contains( INode<T> node );
    bool Contains( T item );

    ITree<T> Cut( T item );
    ITree<T> Copy( T item );
    ITree<T> DeepCopy( T item );
    bool Remove( T item );
}

The indexer property returns the first node that has the specified data element, using the tree's DataComparer. The methods use the indexer to find the required node and then operate on that node.

Enumerators

These interfaces and members allow you to perform enumerations:

public interface IEnumerableCollection<T> : IEnumerable<T>, 
                                                ICollection
{
    bool Contains( T item );
}

public interface IEnumerableCollectionPair<T>
{
    IEnumerableCollection<INode<T>> Nodes { get; }
    IEnumerableCollection<T> Values { get; }
}

partial interface ITree<T> : IEnumerableCollectionPair<T>
{
    IEnumerableCollectionPair<T> All { get; }
    IEnumerableCollectionPair<T> AllChildren { get; }
    IEnumerableCollectionPair<T> DirectChildren { get; }
    IEnumerableCollectionPair<T> DirectChildrenInReverse { get; }
}

partial interface INode<T> : IEnumerableCollectionPair<T>
{
    IEnumerableCollectionPair<T> All { get; }
    IEnumerableCollectionPair<T> AllChildren { get; }
    IEnumerableCollectionPair<T> DirectChildren { get; }
    IEnumerableCollectionPair<T> DirectChildrenInReverse { get; }
}

The IEnumerableCollection<T> interface is the base of all the enumerators. The IEnumerableCollectionPair<T> interface provides the two views of an EnumerablePair: the Nodes and the Values enumerations. Both ITree and INode implement IEnumerableCollectionPair<T>; these implementations return the All EnumerablePair.

The four properties that return EnumerablePairs provide access to differing parts of the tree, or sub-tree under a node.

Serialization

This implementation supports serialization to binary or XML streams:

partial interface ITree<T>
{
    void XmlSerialize( Stream stream );
}

[ Serializable ]
partial class NodeTree<T> : ITree<T>, 
                            INode<T>, ISerializable
{
    public static ITree<T> XmlDeserialize( Stream stream )
}

Binary serialization is provided through the ISerializable interface. To use this method, your would write something like this:

private void BinarySerialize()
{
    using ( Stream stream = File.Open( Filename, 
                      FileMode.Create, FileAccess.Write ) )
    {
        IFormatter f = new BinaryFormatter();

        ITree<Element> tree = CurrentNode.Copy();

        f.Serialize( stream, tree );
    }
}
private ITree<Element> BinaryDeserialize()
{
    using ( Stream stream = File.Open( Filename, 
                         FileMode.Open, FileAccess.Read ) )
    {
        IFormatter f = new BinaryFormatter();

        return ( ITree<Element> ) f.Deserialize( stream );
    }
}

To use binary serialization, your element data type must be marked with the Serializable attribute.

XML serialization is provided by methods exposed in the ITree interface and NodeTree class. To use these methods, you would write something like this:

private void XMLSerialize()
{
    using ( Stream stream = File.Open( Filename, 
                     FileMode.Create, FileAccess.Write ) )
    {
        ITree<Element> tree = CurrentNode.Copy();

        tree.XmlSerialize( stream );
    }
}

private ITree<Element> XMLDeserialize()
{
    using ( Stream stream = File.Open( Filename, 
                        FileMode.Open, FileAccess.Read ) )
    {
        return NodeTree<Element>.XmlDeserialize( stream );
    }
}

To use the XML serialization, your element data type must support standard XML serialization. By default, the XML serializer serializes all public fields and public properties with get and set accessors, which may not be what you want. See the documentation for the XmlSerializer class in MSDN.

Events

The two interfaces, ITree and INode, both expose many events:

partial interface ITree<T>
{
    event EventHandler<NodeTreeDataEventArgs<T>> Validate;
    event EventHandler Clearing;
    event EventHandler Cleared;
    event EventHandler<NodeTreeDataEventArgs<T>> Setting;
    event EventHandler<NodeTreeDataEventArgs<T>> SetDone;
    event EventHandler<NodeTreeInsertEventArgs<T>> Inserting;
    event EventHandler<NodeTreeInsertEventArgs<T>> Inserted;
    event EventHandler Cutting;
    event EventHandler CutDone;
    event EventHandler<NodeTreeNodeEventArgs<T>> Copying;
    event EventHandler<NodeTreeNodeEventArgs<T>> Copied;
    event EventHandler<NodeTreeNodeEventArgs<T>> DeepCopying;
    event EventHandler<NodeTreeNodeEventArgs<T>> DeepCopied;
}

partial interface INode<T>
{
    event EventHandler<NodeTreeDataEventArgs<T>> Validate;
    event EventHandler<NodeTreeDataEventArgs<T>> Setting;
    event EventHandler<NodeTreeDataEventArgs<T>> SetDone;
    event EventHandler<NodeTreeInsertEventArgs<T>> Inserting;
    event EventHandler<NodeTreeInsertEventArgs<T>> Inserted;
    event EventHandler Cutting;
    event EventHandler CutDone;
    event EventHandler<NodeTreeNodeEventArgs<T>> Copying;
    event EventHandler<NodeTreeNodeEventArgs<T>> Copied;
    event EventHandler<NodeTreeNodeEventArgs<T>> DeepCopying;
    event EventHandler<NodeTreeNodeEventArgs<T>> DeepCopied;
}

You can attach to an event at the node or tree level. Every event is raised for the current node, and then for the containing tree. I thought about raising the events for each parent of the current node, but this seemed a bit too much. The default Validate handler checks the type of the data object, and throws an exception if this does not match the type of the tree element.

See Points of interest which explains about using an EventHandlerList object to minimize the space overhead of so many events.

Points of interest

Serialization

Note the use of ISerializable, and the persistence of a version number to help future-proof the serialization process. The default serialization implementation is inflexible, but these two operations go a long way to mitigating its failures. You may like to use a SerializationBinder to return the current types when types from a previous version are being deserialized.

Events

I have made a lot of events available - probably more than anyone will ever need outside of a test application. This would have had an unacceptable increase in the space requirements of the NodeTree class, so I used an EventHandlerList object to minimize the impact. Basically, instead of having a collection for each event in a class, you only have one collection for all events, and use key objects to only record events that are attached. Thus, each instance of the NodeTree just has one instance of EventHandlerList, and this only records attached events. This makes the raising an event a little more complicated, but not very much so.

Version 2: The EventHandlerList is now only created when an event is hooked. This saves about 16 bytes per node when no events are hooked.

Conclusion

This collection is not meant to be a panacea. It favors functionality over efficiency, which has made it quite fat. However, it does fill a gap in the .NET Framework, and is certainly better than using an invisible TreeView. I present it here as another option to add to your toolbox.

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralCongratulations!!!
Carlos Gutierrez
3:53 16 Jan '10  
Big Grin Excelent job!!!!

Carlos Gutiérrez
El conocimiento es libre y debe ser de todos

GeneralRe: Congratulations!!!
Nicholas Butler
6:56 16 Jan '10  
Thanks, Carlos Smile

I hope it's useful.

Nick

----------------------------------
Be excellent to each other Smile

GeneralDifferent Elements as Nodes
hsheldon
8:46 19 Jun '09  
Is there a way to differ the classes in the child nodes/trees compared to the parent tree/node?

For example.
ITree tree = NodeTree.NewTree();

ITree tree2 = NodeTree.NewTree();

tree.AddChild(tree2);
NewsBinary serialization broken in .NET 3.5 SP1
Nick Butler
2:02 22 Jan '09  
There is a fix available:

http://support.microsoft.com/kb/959209[^]

This should be included in the version of SP1 that is pushed through Windows Update.

Nick

----------------------------------
Be excellent to each other Smile

GeneralTree for MLM business,,, any body having code??
Member 4634921
20:39 22 Feb '08  
Hi freinds,
i have one site to build , 5 maximum child is there, in every parent,
is there any code, having u???

plz help

thanking you

sajid.
GeneralPlease add code commnents for intellisense
Member 4668556
3:12 7 Dec '07  
hi,

Great work thank you. I am using this for all my trees.

Please add comments to classes and functions! (Standard visual studio intellisense XML comments!) It took several hours to code the first functions using your class, and I am sure I am still not using all functions due to lack of knowledge of what they do!! Confused

Thanks!
GeneralWhere and how to apply the visitor pattern
Lenny_L_Miller
13:48 15 Mar '07  
Hey,

have you applied a visitor pattern and applied it to this.

Where would I place the Accept(Visitor v) abstract class?

thanks


QuestionEncapsulation
Zajda82
3:18 6 Mar '07  
How about encapsulating your class, for example:


public interface INavigatorTreeItem
{
string Name { get; set; }
}

public class NavigatorTree : Common.NodeTree {
}

how should i work with my new class then?


QuestionCopyright
stevematte
8:37 12 Sep '06  
What is the copyright of this class? May I use it if I'm doing a lucrative project?

smatte
AnswerRe: Copyright
Nicholas Butler
9:21 12 Sep '06  
This is open source software - so you can use it at your own risk ( don't sue me Smile )

If you like it you could vote for the article Blush

----------------------------
Be excellent to each other Smile

EasiReports[^] My free reporting component for WinForms.  

GeneralGetPath
deerchao
23:01 21 May '06  
I think if there is a GetPath method in ITree, that will be helpful:

public List<T> GetPath(T data)
which returns a list including the path from root to the specified node.

This is easy, and sometimes we need it.
GeneralWhy not use a List to store child nodes?
Liu Junfeng
19:12 6 Dec '05  
Instead of
partial interface INode<T>
{
INode<T> Parent { get; }
INode<T> Previous { get; }
INode<T> Next { get; }
INode<T> Child { get; }

INode<T> Root { get; }
INode<T> Top { get; }
INode<T> First { get; }
INode<T> Last { get; }

INode<T> LastChild { get; }
}

I think
partial interface INode<T>
{
INode<T> Parent { get; }
List<INode<T>> ChildNodes { get; }
}
is better to manipulate.
AnswerRe: Why not use a List to store child nodes?
Nicholas Butler
21:34 6 Dec '05  

Hi

There are a few reasons I didn't do it the way you suggest:

1) It's a port of my Tree Collection for .Net 1, and List<> wasn't available then.

2) I wanted to do it from scratch, so I kept control.

3) List<> has an overhead of 28 bytes, which would double the space overhead of the NodeTree class.

4) List<> uses an array internally, which makes manipulating nodes slower.

Basically, it just didn't seem like a good idea Smile


----------------------------
Be excellent to each other Smile
GeneralNice one!
CodeGimp
3:06 5 Dec '05  
Great article, nice design (twin interface implementation on NodeTree class was rather cunning IMHO) and well written.

Good work fella!
AnswerThanks
Nicholas Butler
4:11 5 Dec '05  

Big Grin

----------------------------
Be excellent to each other Smile
GeneralAVL Tree
Ayrezyle
5:45 30 Nov '05  
Nice!

For those who want, here's a self-balancing (AVL or Red-Black) tree implementation:

http://www.codedebate.com/source/AvlTree.cs.html

- Anton Delsink
GeneralVS 2005
GHoffer
14:54 29 Nov '05  
Have you looked at System.Collections.Generic.LinkedList?

Gary G
GeneralBeware of Trees on Modern Processors
John Melville
14:35 23 Nov '05  
The article questions why microsoft has omitted such an "obvious" element from their toolkit. The question has been answered several times by Microsoft performance architects Raymond Chen and Rico Marinari

The simple answer is trees are a really bad idea on modern processors. A cache miss causes the processor to stall for hundreds of cycles to hit main memeory. Once you are working on a high speed processor with a cache, which is every processor now, locality of memory is the name of the game. Trees have no locality and they trash the cache. (The only worse thing is a linked list, which they also don't supply.)

Modern designs focus on resizeable arrays and hashtables, both of which trade (relatively cheep) memory for the locality that makes the processor sing. Microsoft provides excellent implementations for both.

If you have one of the very rare projects that actually needs trees, check out Wintellect's power collections.NET. This STL inspired open source project was begun by some ex-microsofties, and proper cache management is pervasive throughout their designs.

John Melville
AnswerRe: Beware of Trees on Modern Processors
Nicholas Butler
1:10 24 Nov '05  

Thanks for your feedback.

As I said in the article, this collection is not meant to be a panacea; it is just another option to add to your toolbox.

BTW, the PowerCollections project only has a red-black tree - it doesn't have a simple tree.

If you need a simple tree, I have done the leg-work for you.


----------------------------
Be excellent to each other Smile
GeneralRe: Beware of Trees on Modern Processors
Paulustrious
4:53 14 Jan '06  
Many thanks. I was in the process of re-inventing the wheel. It was, unfortunately, nowhere near as round as yours and would have taken me a long time.

The performance issue of large trees depends on out-of-cache hits. This can be improved by replacing the 'new' constructor.

Allocate an array and grab from the array. When there is no more to grab allocate another bunch. Only the 'user' can signal a logical transition and say...

"If you want to release the rest of the block and start another, now is a good time to do it. But you might want to hang on to a bit in case we have children"

That's a bit long for a method name but you know what I mean.

This is the painful process we used to go through on mainframes with Page and Swap files taking the place of non-cache hits. The process of breaking and recreating chains in single allocation blocks provides an overhead, but for long-running processes it may be advantageous. There tends to be a critical size when all hell breaks loose and performance dies. This harkens back to when IBM meant Invest in a Bigger Machine.


Paulustrious

Paulustrious
Email: codeproject @who@is@at@ paulcotter.com
GeneralRe: Beware of Trees on Modern Processors
Kastellanos Nikos
22:21 29 Nov '05  
John Melville wrote:
The simple answer is trees are a really bad idea on modern processors.

So, what? If i need a tree structure for my project i am gona do just that. Should i tell my boss/client that this project is a really bad idea? D'Oh!
What kind of logic is this. You make no sence.


Generalefficiency with very large numbers of nodes/leaves
BillWoodruff
18:25 22 Nov '05  
Just curious if you have done any experiments with looking at memory use and access time for very large trees with many nodes and leaves ?

Nice work !

Bill Woodruff

"The greater the social and cultural distances between people, the more magical the light that can spring from their contact." Milan Kundera in Testaments Trahis
AnswerRe: efficiency with very large numbers of nodes/leaves
Nicholas Butler
3:35 23 Nov '05  
I have just tried some basic experiments.

Some operations are O(1) - e.g. InsertChild. This takes about 0.001 ms per node.

Other operations are O(n) - e.g. AddChild. This takes about 0.001 ms per node when n = 100, 0.01 ms per node when n = 1000, and continues to increase as you add more nodes.

The space overhead of each node is 44 bytes. 20 bytes of this is the EventHandlerList, so if you can do without events and are worried about space, you could just take them out. I'll update the code to make this an option.


----------------------------
Be excellent to each other Smile
AnswerRe: efficiency with very large numbers of nodes/leaves
Nicholas Butler
5:39 24 Nov '05  

I have updated the code - now version 2.

I have used lazy instantation for the EventHandlerList. This saves 16 bytes in each node that doesn't have any events attached, and brings the overhead per node to 28 bytes.

This is made up like this:

4 bytes: Parent reference
4 bytes: Previous reference
4 bytes: Next reference
4 bytes: Child reference
4 bytes: EventHandlerList reference
8 bytes: Class overhead
I think that's about as small as I can easily make it.

----------------------------
Be excellent to each other Smile
GeneralDood...you rock.
Marc Leger
23:02 20 Nov '05  
Seriously, thank you. Know of any good C# -> VB.NET translators for 2.0? Smile


Last Updated 20 Nov 2005 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010