Click here to Skip to main content
15,860,859 members
Articles / Programming Languages / C#
Article

How to fill hierarchical data into a TreeView using base classes and data providers.

Rate me:
Please Sign up or sign in to vote.
4.38/5 (31 votes)
11 Mar 2004CPOL9 min read 248.4K   3.9K   141   38
This tutorial shows how to use the System.Windows.Forms.TreeView control in a way that the end user and the developer can have phun at the end of the day.

Sample Image - treeview.gif

Introduction

This tutorial shows how to use the System.Windows.Forms.TreeView control in a way that the end user and the developer can have phun at the end of the day.

Basically, I would like to talk about:

  • Load On Demand
  • Recursive method calls for hierarchical data structures
  • The use of DataProviders

developer view

Before I start to describe the data, I would like to note that all developers like to write code that they don't have to change often. They like to write code which they can reuse later to develop straight forward on a tested code base.

If we now create a TreeView class, for instance, which meets exactly the requirements for our current project, the chances to reuse the same code later in another project are <=0. We have to find a generic way to implement the project related stuff separated and reuse our tested TreeView class which provides only the most common features.

That's where the IDataProvider interface comes in. With data providers, we can reuse our tested TreeView code for new projects which require different specific features and behaviors.

Normally, you will have to fill data into a tree view which reflects a hierarchical data schema. Typically, your data contains a text and ID field, which should be used as underlying data for your tree nodes. This example uses an Access database which is delivered in the sample application and makes use of this schema and data:

Image 2

As you can see, we have items with a text field identified by an ID field, which can hold sub items identified by a parent_id field. The root items have typically no parent_id or values like null or -1. Now, this looks like real world data to me, I guess there are thousands of data tables like this one out there, and typically these ones are filled into a tree view. But hey, which user would like to wait until all rows have been filled, it's not a good idea to try to load all rows into a tree view. Because...

User view

The user expects a fast and short tree view handling, that you can't provide if you fill all items at once, we have to fill on demand. This means we fill all the items the user expands during his work. The user don't need all items for his work, only a few ones, which he expand automatically during his work. To load only a few items is a fast operation and gives the users a good experience about working with the software.

Design

Overview

After this short analysis, we should notice the following requirements:

  • We need to use a fill mechanism that can work on different item levels (for instance root items, sub items...).
  • We need a way to assign a + sign to our (empty) parent nodes which we would like to fill later, otherwise the user doesn't realize that he can expand the node.
  • We need to declare a data provider interface.

This UML diagram shows the classes created to design the tutorial base classes.

Image 3

TreeNodeBaseTreeNodeBase inherits from TreeNode and provides methods to handle dummy child nodes for filling on demand operations.
TreeViewStrategyThe TreeViewStrategy is tree view which aggregates a tree view data provider interface (Strategy pattern). The goal of this design is to provide an easy way to extend or add data providers without changing a single line of tree view code. Basically, the tree view interface will not change so far, but the data providers will change their behavior and features.
IDataProviderDataProvider interface is based on TreeViewStrategy and TreeNodeBase and it is responsible to fill a TreeViewStrategy instance with the requested data.

But, let's take a closer look at the classes now...

Base classes

TreeNodeBase

First, we create a new class which inherits from System.Windows.Forms.TreeNode. Let us add the required handling for dummy nodes to our base tree node class. Dummy nodes are used to assign a + sign to an empty parent node. An empty parent node is a node, which must have a + sign because we exactly know that there are sub items to be filled later.

C#
public class TreeNodeBase : System.Windows.Forms.TreeNode
{
Public Instance Properties
C#
/// Gets a value indicating if this node owns a dummy node.
public virtual bool HasDummyNode
{
    get
    {
        return (Nodes.Count>0 && Nodes[0].Text == "@@Dummy@@");
    }
}
Public Instance Methods
C#
    /// Adds a dummy node to the parent node
    public virtual void AddDummyNode()
    {
        Nodes.Add(new TreeNodeBase("@@Dummy@@"));
    }
    /// Removes the dummy node from the parent node.
    public virtual void RemoveDummyNode()
    {
        if ((Nodes.Count == 1 ) & (Nodes[0].Text == "@@Dummy@@"))
        {
            Nodes[0].Remove();
        }
    }    
}

Every TreeNodeBase instance can now be easily made an empty parent node, by calling AddDummyNode(). If we would like to know if a node instance has currently a dummy node assigned, we can determine the return value of HasDummyNode. To get rid of our dummy node, we just call RemoveDummyNode().

TreeViewStrategy

Now, we create a new class which inherits from System.Windows.Forms.TreeView.

C#
public class TreeViewStrategy : System.Windows.Forms.TreeView
{
Public Instance Fields
C#
/// <SUMMARY>Fired if the datasource has changed</SUMMARY>
public EventHandler DataSourceChanged;
/// <SUMMARY>The underlying data provider which is
/// used to start requests.</SUMMARY>
private IDataProvider _dataProvider;
Constructor

Because our IDataProvider is responsible to handle the ContextMenu, we have to create and assign a ContextMenu instance to our TreeView.

C#
/// Initializes a new instance of the TreeViewStrategy class.
public TreeViewStrategy()
{
    // create the context menu and assing it to the tree view
    ContextMenu = new ContextMenu();
    ContextMenu.Popup += new EventHandler(ContextMenu_Popup);
}
Public Instance Properties

The DataSource property will let us attach a IDataProvider implementation later.

C#
/// Gets or sets the IDataProvider which is responsible
/// to manage this <SEE cref="TreeView" /> instance.
public IDataProvider DataSource
{
    get
    {
        return _dataProvider;
    }
    set
    {
        _dataProvider = value;
        // fire the DataSourceChanged event
        OnDataSourceChanged(EventArgs.Empty);
    }
}
Public Instance Methods

The Fill method is called to fill the root level of the tree view. Note that the Fill method will handle the WaitCursor and clears all the nodes before it calls a method on the IDataProvider interface which really does the work behind the scene.

C#
/// Fill's the root level.
public void Fill()
{
    System.Diagnostics.Debug.Assert(_dataProvider!=null);
    // show wait cursor, maybe there is
    // a longer operation on the data provider
    Cursor.Current = Cursors.WaitCursor;
    try
    {
        // clear all nodes
        Nodes.Clear();
        // request the new root nodes
        _dataProvider.RequestRootNodes(this);
    }
    finally
    {
        // Make sure to reset the current cursor.
        // NOTE: There is no catch block, because we don't
        // want to handle occured exceptions at this level,
        // the user of this code is responsible
        // to manage it.
        Cursor.Current = Cursors.Default;
    }
}

This class provides a DataSourceChanged event. It's a commonly used technique to provide a virtual method which internally fires the event for us. This way, it's possible for inheritors to consume this event in a derived class without subscribing to our delegate, by overriding the method.

C#
/// Raises the DataSourceChanged event.
protected virtual void OnDataSourceChanged(EventArgs e)
{
    if(DataSourceChanged!=null) DataSourceChanged(this, e);
}

Because the IDataProvider interface is responsible to handle the ContextMenu, we should serve a virtual method for the ContextMenu.Popup event which we delegate to the IDataProvider. To do this, we have to create a ContextMenu for our TreeView and consume its ContextMenu.Popup event in our class and call this virtual method.

C#
/// Invokes the <SEE cref="IDataProvider.QueryContextMenuItems" /> method
protected virtual void OnContextMenuPopup(EventArgs e)
{
    System.Diagnostics.Debug.Assert(_dataProvider!=null);
    // retrieve node which was clicked
    TreeNodeBase node =
      GetNodeAt(PointToClient(Cursor.Position)) as TreeNodeBase;
    if(node==null) return;
    // clear previous items
    ContextMenu.MenuItems.Clear();
    // let the provider do his work
    _dataProvider.QueryContextMenuItems(this.ContextMenu, node);
}

This event enables load on demand, because the event is fired every time the user expands a node. Because we're only dealing with TreeNodeBase instances, we can safely cast e.Node. If the node has a dummy node, we can fill the node now with the real child nodes. To do this, we remove the dummy node and let the IDataProvider fill the child nodes. Note that this method also handles the WaitCursor.

C#
/// Invokes the <SEE cref="IDataProvider.RequestNodes" /> method
protected override void OnBeforeExpand(TreeViewCancelEventArgs e)
{
    // cast node back to our base type
    TreeNodeBase node =  (TreeNodeBase)e.Node;
    System.Diagnostics.Debug.Assert(node!=null);
    // fill expanded node if not filled at this time (on demand)
    if(node.HasDummyNode)
    {
        // remove dummy node before we fill the child nodes
    // and let the node expand itself to show
        // it's children nodes
        node.RemoveDummyNode();
        //
        Cursor.Current = Cursors.WaitCursor;
        try
        {
            // let the provider do his work
            _dataProvider.RequestNodes(this, node, e);
        }
        finally
        {
            Cursor.Current = Cursors.Default;
        }
    }
    //
    base.OnBeforeExpand (e);
}

We handle the ContextMenu.Popup event with a private event handler method.

C#
/// Fired by the <SEE cref="ContextMenu" />
/// before the shortcut menu is displayed.
/// The private event handler is used to call the virtual
/// protected method <SEE cref="OnContextMenuPopup" />
private void ContextMenu_Popup(object sender, EventArgs e)
{
    OnContextMenuPopup(e);
}

Don't forget to close the bracket, otherwise your compiler will let you know about messy things :).

C#
}

IDataProvider

Now, we are ready to define the IDataProvider interface. This interface is responsible to fill our TreeView and to query ContextMenu items.

C#
/// DataProvider interface is based on TreeViewStrategy and TreeNodeBase and
/// it is responsible to fill a <SEE cref="TreeViewStrategy" />
/// instance with the requested data.
public interface IDataProvider
{
    /// Request to fill the root nodes.
    void RequestRootNodes(TreeViewStrategy treeView);
    /// Request to fill child nodes.
    void RequestNodes(TreeViewStrategy treeView, 
        TreeNodeBase node, TreeViewCancelEventArgs e);
    /// Request to fill a <SEE cref="ContextMenu" />
    /// with <SEE cref="MenuItem" />'s for the
    /// specified <SEE cref="TreeNode" />.
    void QueryContextMenuItems(ContextMenu contextMenu, TreeNodeBase node);
}

Now, we can start to write different IDataProvider implementations which work together with the TreeView class without touching the TreeView code in the future to support new features and behaviors.

Sample Application

The sample application provides a IDataProvider implementation and a demo form which makes use of our TreeViewStrategy class. I used a strongly typed DataSet class to retrieve the Access database data. Basically, my IDataProvider interface implementation retrieves the data from the DataSet and creates associated nodes for the tree view.

DataBaseNodeInfo

The first step was to create a TreeNode class which reflects the current database schema. The node has to provide ID and text property. The ID property has to be readonly. So, I defined a struct to hold the database text and ID values.

C#
public struct DataBaseNodeInfo
{
Public Instance Fields
C#
/// <SUMMARY>Holds the text</SUMMARY>
public string Text;
/// <SUMMARY>Hodsl the id</SUMMARY>
public readonly int Id;
Constructor
C#
    /// Initializes a new instance of the DataBaseNodeInfo class.
    public DataBaseNodeInfo(string text, int id)
    {
        Text = text;
        Id = id;            
    }
}
TreeNodeDatabase

And a TreeNode class which inherits from TreeNodeBase which uses the struct as underlying data source. This is a clean way to deal with database items, without referencing any data row class that was generated from the strongly typed DataSet wizard. Note the internal constructor which is only used to create dummy nodes.

C#
public class TreeNodeDatabase : Raccoom.Windows.Forms.TreeNodeBase
{
Public Instance Fields
C#
/// <SUMMARY>holds the underlying data
/// in a <SPAN lang=de-ch>simple struct</SUMMARY>
private DataBaseNodeInfo _dbInfo;
Constructor
C#
/// Initializes a new instance of the TreeNodeDatabase class.
internal TreeNodeDatabase(string text) : base(text)
{
    _dbInfo = new DataBaseNodeInfo(text, -2);
}
/// Initializes a new instance of the TreeNodeDatabase class.
public TreeNodeDatabase(DataBaseNodeInfo dbInfo) : base(dbInfo.Text)
{
    _dbInfo = dbInfo;
}
/// Initializes a new instance of the TreeNodeDatabase class.
public TreeNodeDatabase(string text, int id) : base(text)
{
    this._dbInfo = new DataBaseNodeInfo(text, id);
}
Public Instance Properties
C#
    /// Gets the node Id.
    public int Id
    {
        get
        {
            return _dbInfo.Id;
        }
    }
    /// Gets or sets the text property
    new public string Text
    {
        get
        {
            return _dbInfo.Text;
        }
        set
        {
            _dbInfo.Text = value;
        }
    }
}

Now we have completed the basic classes that can deal with the database data in a proper manner. The only thing that is missing is the IDataProvider implementation, and I guess the only reason that you don't have stopped reading is that you would like to know how to implement it, right? :)

DataProviderDatabase

So, let's dive into the code, the code that controls the core process, retrieving data. To give you an example how the data provider can extend the existing TreeView with new features, this data provider contains a search based on node ID.

C#
/// DataProviderDatabase implements the
/// <SEE cref="IDataProvider" /> interface
/// and it supports hiearchical database schemas.
public class DataProviderDatabase : IDataProvider
{
Public Instance Fields
C#
/// <SUMMARY>DataTable which holds the data to fill</SUMMARY>
Raccoom.Sample.DataSet1.treeview_dataDataTable _table = null;
Constructor
C#
/// Initializes a new instance of the DataProviderDatabase class.
public DataProviderDatabase(){}
Public Instance Properties

The filled DataTable which the data provider internally uses as data source:

C#
/// Gets or sets the DataTable which holds the data to fill.
public Raccoom.Sample.DataSet1.treeview_dataDataTable DataTable
{
    get
    {
        return _table;
    }
    set
    {
        _table = value;
    }
}
Public Instance Methods

Interface method which calls the private Fill method. To retrieve the child nodes, the database ID provided by the current node, which is the parent node, is used.

C#
/// Request to fill child nodes.
public void RequestNodes(TreeViewStrategy treeView,
         TreeNodeBase node, TreeViewCancelEventArgs e)
{
    System.Diagnostics.Debug.Assert(node is TreeNodeDatabase);
    // fill the childs from data source (table)
    Fill(node.Nodes, ((TreeNodeDatabase)node).Id);
}

Interface method which also calls the private Fill method. Note: this method works for both, root and sub nodes.

C#
/// Request to fill the root nodes.
public void RequestRootNodes(TreeViewStrategy treeView)
{
    System.Diagnostics.Debug.Assert(DataTable!=null);
    // Fill the root level
    Fill(treeView.Nodes, -1);
}

Simple create some context menu items for the demo:

C#
/// Request to fill aContextMenu
/// with MenuItem's for the specified TreeNode.
public void QueryContextMenuItems(ContextMenu contextMenu,
                                             TreeNodeBase node)
{
    contextMenu.MenuItems.Add("&Open " + node.Text);
    contextMenu.MenuItems.Add("&Edit "+ node.Text);
    contextMenu.MenuItems.Add("&Delete "+ node.Text);
}

This method deals with a TreeNodeCollectoin, because every TreeNode and either the TreeView provides a TreeNodeCollection. It is more generic to pass this collection as a parameter, otherwise you can't use this method recursively. Because, you have to provide a method to deal with the root nodes, and a special one to deal with a specific node. But if you deal with collections, you can call your method recursively, no matter which level you are currently in. This works for n levels.

Maybe, a possible newbie question can be: I can't provide nested for loops for each level, because I don't know at design time how deep my data structure will be. But with this kind of method, you don't have to mess around with such stuff, because the recursive design will work for every level.

C#
/// Does a recursive search looking for the node
/// that represents the specified database id
/// starting by specified root collection.
public TreeNodeDatabase FindNodeById(TreeNodeCollection col, int id)
{
    // go through collection and compare
    // each node.id which against the db id.
    foreach(TreeNodeDatabase node in col)
    {
        if(object.Equals(node.Id, id)) return node;
        // call this method recursive to process
        // all nodes in the treeview.
        TreeNodeDatabase subNode = FindNodeById(node.Nodes, id);
        if(subNode!=null) return subNode;
    }
    return null;
}

The Fill method also use a TreeNodeCollection as input parameter, this way I can create root nodes and child nodes. It makes no difference, only the parent_id is different. To decide if the currently created node is a parent node, I use a select which determines if there are any children associated with this database item. If there are any child rows, the currently created node gets a dummy node associated.

C#
    /// Retrieves the child nodes for the given node
    /// and add's them to the node childs collection
    private void Fill(TreeNodeCollection col, int parent_id)
    {            
        foreach(Raccoom.Sample.DataSet1.treeview_dataRow 
            row in _table.Select("parent_id = "+parent_id))
        {
            TreeNodeDatabase node = 
                new TreeNodeDatabase(row.node_text, row.id);
            col.Add(node);
            // count on childs, if node is parent node,
            // add a dummy node for + sign
            if(_table.Select("parent_id = "+node.Id).Length>0)
            {
                node.AddDummyNode();
            }
        }
    }
}

Possible improvements

  • DataProvider provides ImageList property.
  • TreeNodeBase provides Refresh method and a related CanRefresh property.

Conclusions

This is my first tutorial and I hope it is somewhat useful for others. Keep in mind that this doesn't have to be the only or best way to deal with a TreeView, but it's the best I know of. If you would like to see more code based on this design, take a look here.

As always, any feedback or criticism is appreciated.

Links

History

  • 12.03.2004 - Initial release.

Have phun...

License

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


Written By
Software Developer (Senior)
Switzerland Switzerland
My interest is in the future because I am going to spend the rest of my life there. (Charles Kettering)

Biography

  • 1996 - 1998 PC Board PPL, HTML, DHTML, Javascript and ASP
  • 1999 - 2001 coding Centura against Sql Database (SqlBase,MSSQL,Oracle)
  • 2002 - 2004 C# Windows Forms
  • 2005 - 2006 C# ASP.NET, Windows Forms
  • 2006 - 2009 C#, WCF, WF, WPF
  • 2010 - 2012 C#, Dynamics CRM, Sharepoint, Silverlight
  • 2013 - 2013 C#, WCF DS (OData), WF, WPF
  • 2014 - 2016 C#, Azure PaaS, Identity, OWIN, OData, Web Api
  • 2017 - now C#, aspnet.core, IdentityServer4, TypeScript & Angular @ Azure IaaS or PaaS

Interests

  • family & friends
  • chilaxing ,)
  • coding

Comments and Discussions

 
GeneralRe: Images OK Pin
Chris Richner20-Apr-04 13:14
Chris Richner20-Apr-04 13:14 
QuestionThe images ? Pin
Antonio Barros18-Mar-04 6:55
professionalAntonio Barros18-Mar-04 6:55 
AnswerRe: The images ? Pin
Chris Richner18-Mar-04 12:56
Chris Richner18-Mar-04 12:56 
GeneralRe: The images ? Pin
Atao518-Mar-04 21:16
Atao518-Mar-04 21:16 
GeneralRe: The images ? Pin
Chris Richner19-Mar-04 15:44
Chris Richner19-Mar-04 15:44 
GeneralMore possible improvements... Pin
Chris Richner17-Mar-04 12:09
Chris Richner17-Mar-04 12:09 
GeneralRe: More possible improvements... Pin
LZF13-Jul-04 5:13
LZF13-Jul-04 5:13 
GeneralRe: More possible improvements... Pin
Chris Richner13-Jul-04 18:24
Chris Richner13-Jul-04 18:24 
Hi LZF,

I think you can separate the code, but then there is no layer between to create nodes for the treeview, that is the only downside i can see now. Let me know if you develop some enhancements you would like to share.

myMsg.BehindDaKeys = "Jerry Maguire";

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.