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






4.38/5 (29 votes)
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.
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:
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.
TreeNodeBase |
TreeNodeBase inherits from TreeNode and provides methods to handle dummy child nodes for filling on demand operations. |
TreeViewStrategy |
The 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. |
IDataProvider |
DataProvider 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.
public class TreeNodeBase : System.Windows.Forms.TreeNode
{
Public Instance Properties
/// 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
/// 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
.
public class TreeViewStrategy : System.Windows.Forms.TreeView
{
Public Instance Fields
/// <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
.
/// 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.
/// 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.
/// 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.
/// 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.
/// 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
.
/// 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.
/// 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 :).
}
IDataProvider
Now, we are ready to define the IDataProvider
interface. This interface is responsible to fill our TreeView
and to query ContextMenu
items.
/// 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.
public struct DataBaseNodeInfo
{
Public Instance Fields
/// <SUMMARY>Holds the text</SUMMARY>
public string Text;
/// <SUMMARY>Hodsl the id</SUMMARY>
public readonly int Id;
Constructor
/// 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.
public class TreeNodeDatabase : Raccoom.Windows.Forms.TreeNodeBase
{
Public Instance Fields
/// <SUMMARY>holds the underlying data
/// in a <SPAN lang=de-ch>simple struct</SUMMARY>
private DataBaseNodeInfo _dbInfo;
Constructor
/// 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
/// 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.
/// DataProviderDatabase implements the
/// <SEE cref="IDataProvider" /> interface
/// and it supports hiearchical database schemas.
public class DataProviderDatabase : IDataProvider
{
Public Instance Fields
/// <SUMMARY>DataTable which holds the data to fill</SUMMARY>
Raccoom.Sample.DataSet1.treeview_dataDataTable _table = null;
Constructor
/// 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:
/// 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.
/// 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.
/// 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:
/// 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.
/// 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.
/// 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
providesRefresh
method and a relatedCanRefresh
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
- Microsoft patterns & practices
- Converting Solutions from Visual Studio .NET 2002 - 2003
- Enhanced BrowseForFolder styled TreeView
- Browse technical helpfile online
History
- 12.03.2004 - Initial release.
Have phun...