Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DataBound TreeView Control

0.00/5 (No votes)
6 Jan 2005 1  
A way to bind up a simple TreeView control.

Sample Image

Introduction

I looked around for some time, trying to find a decent databound treeview control. I came across the same story time and time again, that due to inherent design considerations surrounding the TreeView control, it wasn't possible for Microsoft to tie the control into the DataBinding framework. While I believe this to be true, I also thought that in some simple implementations of the control, databinding indeed would be possible. I first came across an article by Duncan McKenzie of Microsoft.

Duncan's article was written in VB.NET and was later rewritten in C# by LZF. I'd suggest both these articles for anyone interested in the original ideas. I took from both of these articles, and developed a solution that I'm able to use easily with just the control and a DataSet. Of course, there will ultimately be situations that break my solution, but for now, it's working well. I look forward to your critiques.

Background

Here's what I wanted... I wanted to present a TreeView control, with a basic hierarchical DataSet in the form of a DataSource property, and have that DataSet be represented by a TreeView in accordance with the DataRelations in the DataSet. I needed to be able to control the DisplayMember and the ValueMember for each node level in the tree. Further, there was the inevitable need to supply the designer with ImageList, ImageIndex, SelectedImageIndex properties.

Navigationally, I had to make sure that when a node was selected, all corresponding bound controls would receive the message through their currency managers. That is to say, if I moved in the tree, bound controls should also move. I used the AfterSelect event of the TreeView to wire up this activity. From the other side, I needed to move the selected node of the tree if the selected DataRow were moved from another control (i.e., DataGrid). In other words, if the user moved the position in a DataGrid, it should also move the selected node of the tree.

Data synchronization required that if a DataRow object were updated anywhere in the UI, the display member of the TreeView would also be updated. This obviously only affected the DataRow columns that were being used in the Text property of the TreeNode.

Using the code

I've provided a simple solution that shows how everything works together. Have fun with it.

The sample code provides a typed dataset that can be used with the Northwind database. The demonstration will use the classic hierarchy of Customer, Order, OrderDetail. Make sure your SqlConnection object is attached to a Northwind database. The SqlDataAdapters should already be setup to load the typed dataset, and the relations are already in the DataSet.

Most of the code here is simple, but note how we manually create an array of TableBinding objects that describe the use and presentation of each table in the DataSet. If we don't specify the Table Name, Display Member and Value Member, the control will use the first column in each table for display and value. You may also specify the ImageList, ImageIndex and SelectedImageIndex properties before loading the tree.

The only thing to do then is to call LoadTree with a DataSet and TableBinding array. After loading the tree, I threw up some DataGrids to allow navigation and edit of the DataSet.

private NorthwindDataSet ds;

private void Form1_Load(object sender, System.EventArgs e)
{
    ds = new NorthwindDataSet();
}
        
private void button1_Click(object sender, System.EventArgs e)
{

    // Fill up the DataSet

    this.sqlDataAdapter3.Fill(ds, "Customers");
    this.sqlDataAdapter4.Fill(ds, "Orders");
    this.sqlDataAdapter5.Fill(ds, "OrderDetails");

    // Create an array of TableBindings that

    // define Table Name, Value Member and Display Member

    TableBinding[] tableBindings = new TableBinding[] {
        new TableBinding("Customers", "CustomerID", "CompanyName"), 
        new TableBinding("Orders", "OrderID", "OrderID"), 
        new TableBinding("OrderDetails", "ProductID", "ProductID")};


    // Setup the initial TreeView defaults

    treeResource.TreeView.HideSelection = false;
    treeResource.TreeView.ImageList = this.imageList1;
    treeResource.TreeView.ImageIndex = 0;
    treeResource.TreeView.SelectedImageIndex = 1;

    // Load up the Tree

    treeResource.LoadTree(ds, tableBindings);

    // Load up the DataGrids

    this.dataGrid1.DataSource = ds;
    this.dataGrid1.DataMember = "Customers";
    this.dataGrid2.DataSource = ds;
    this.dataGrid2.DataMember = "Customers.CustomersOrders";
    this.dataGrid3.DataSource = ds;
    this.dataGrid3.DataMember = "Customers.CustomersOrders.OrdersOrderDetails";

}

Run the example and press the Load Tree button on the left. Play around with the navigation and see that you get what you expect. Try pressing the second Load Tree button...

Notice on loading the second tree, I call SetEvents(ds, false) before LoadTree. This is so that the first tree doesn't navigate to each node. Don't forget to turn navigation back on with SetEvents(ds, true). You will notice the DataGrids updating.

private void button2_Click(object sender, System.EventArgs e)
{
    TableBinding[] tableBindings = new TableBinding[] { 
        new TableBinding("Customers", "CustomerID", "CompanyName"), 
        new TableBinding("Orders", "OrderID", "OrderID"), 
        new TableBinding("OrderDetails", "ProductID", "ProductID")};

    treeResource2.TreeView.HideSelection = false;
    treeResource2.TreeView.ImageList = this.imageList1;
    treeResource2.TreeView.ImageIndex = 0;
    treeResource2.TreeView.SelectedImageIndex = 1;

    // I turn off the events for the first tree so that loading the second

    // tree doesn't navigate to every node in the first tree.

    // You'll notice that the DataGrids do move... You could remove the DataSource

    // from them temporarily to eliminate that as well.

    treeResource.SetEvents(ds, false);
    treeResource2.LoadTree(ds, tableBindings);
    treeResource.SetEvents(ds, true);
}

I cut out the gist of the control's logic to show you here. More explanation may be required, but if you understand this, the rest will follow. I said in the "Background" that I needed the selection of nodes in a tree to reflect a corresponding change in position on the affected currency managers. This is where I accomplish that, in the tv_AfterSelect event that's wired up to the TreeView.

Because selecting any node in the tree (other than a leaf node) will create a different IBindingList on the CurrencyManagers, we will reposition each CurrencyManager starting with the highest parent node. Thinking about the ways you can select a node, it becomes apparent that you could, for example, select a child for a different parent by expanding another node (without first selecting that node) and then selecting a new child. When this happens, you must first select that new node's ancestry, so we create a nodeList array and add to it the selected node and its ancestors. Once we have that, we just loop through starting at the oldest (highest) node.

We also disable the PositionChanged event with DisablePositionChanged boolean field, otherwise you're in a reciprocal loop between cm_PositionChanged which moves the TreeView (AfterSelect) and tv_AfterSelect which moves the CurrencyManager (PositionChanged). The other handler we remove is the ListChanged, because like I said, every time a new node is selected, the CurrencyManager's lists are newly created.

// When the BoundTreeView's nodes are selected,

// we must synchronize the CurrencyManagers...

private void tv_AfterSelect(object sender, TreeViewEventArgs e)
{
    // We have to move the currency manager positions for every node in the

    // selected heirarchy because the parent node selection determines the

    // currency manager "list" contents for the children

    ArrayList nodeList = new ArrayList();

    // Start with the node that has been selected

    BoundTreeNode node = (BoundTreeNode)((TreeView)sender).SelectedNode;
    nodeList.Add(node);

    // Recursively add all the parent nodes

    node = (BoundTreeNode)node.Parent;
    while (node != null)
    {
        nodeList.Add(node);
        node = (BoundTreeNode)node.Parent;
    }

    // Don't fire the our own position

    // change event other controls bound to the

    // currency managers will move accordingly

    // because we are setting the position

    // explicitly

    DisablePositionChanged = true;

    // Start at the highest parent node

    for (int i = nodeList.Count; i > 0; i--)
    {
        node = (BoundTreeNode)nodeList[i-1];

        ((IBindingList)node.CurrencyManager.List).ListChanged 
                                       -= handlerListChanged;
        node.CurrencyManager.Position = node.Position;
        ((IBindingList)node.CurrencyManager.List).ListChanged 
                                       += handlerListChanged;

    }
    DisablePositionChanged = false;

}

Also, we stated that if the DataSet is moved from another bound control (i.e., DataGrid), the corresponding node should be selected in the TreeView. As long as the other controls are bound to the same CurrencyManager ("precisely" with the same Navigation Path), the BoundTreeView control is notified of a position change. Here, we handle it by making sure we're navigating to an existing row (cm.Position >= 0) and that the DataRow is attached to the DataTable.

From there, we build up a hierarchy of parent rows. Once those are determined, we can iterate the array and find corresponding nodes in the tree. This is done by comparing the value in the DataRow's value column to the BoundTreeNode's tag value in the SelectNode method. After a row is found in the tree, we then only need to search its subnodes to find the remaining nodes. (Only search the lineage of one parent.) This may seem a bit confusing, but if you follow the code, it should become clearer.

Finally, we again handle the attach and detach of events, and as new IBindingLists are created, new ListChanged handlers are attached.

// When the CurrencyManagers change

// position we must reposition the TreeView...

private void cm_PositionChanged(object sender, EventArgs e)
{
    // We manually disable this if we are changing position from tv_AfterSelect

    if (!DisablePositionChanged)
    {
        CurrencyManager cm = (CurrencyManager)sender;

        // The position may be -1 if the currency manager list is empty

        if (cm.Position >= 0)
        {
            DataRowView drv = (DataRowView)((DataView)cm.List)[cm.Position];
            DataRow dr = drv.Row;

            // other controls (DataGrid) may

            // allow adding rows that are unaccessible

            if (dr.RowState != DataRowState.Detached)
            {
                // Start with the data row that was selected

                ArrayList dataRows = new ArrayList();
                dataRows.Add(dr);

                // We have to select the parents

                // first so that we only search the 

                // specific lineage when we call SelectNode

                while (dr.Table.ParentRelations.Count > 0)
                {
                    dr = dr.GetParentRow(dr.Table.ParentRelations[0]);
                    dataRows.Add(dr);
                }

                // Start searching the tree with the base nodes collection

                TreeNodeCollection nodes = _treeView.Nodes;
                TreeNode node = null;
                TableBinding tableBinding;

                // Select the highest parent

                // and then the subsequent children from

                // the returned node's collection of children


                // Start with the highest datarow in the heirarchy

                for (int i = dataRows.Count; i>0; i--)
                {
                    dr = (DataRow)dataRows[i-1];

                    // TableBinding tells us what the field

                    // in the datarow is that will be

                    // compared to the tag value in the node

                    tableBinding = GetBinding(dr.Table.TableName);
                        
                    // Find the node and then search

                    // it's children for the next datarow

                    if (tableBinding != null)
                        node = SelectNode(dr[tableBinding.ValueMember], 
                                                                nodes);
                    else
                        node = SelectNode(dr[0], nodes);

                    // The next nodes collection to search

                    nodes = node.Nodes;
                }

                // We're going to move the tree node

                // selection here, but we don't want

                // the AfterSelect event to be handled

                // because it would fire the

                // currency manager PositionChanged event reciprocally

                _treeView.AfterSelect -= handlerAfterSelect;
                _treeView.SelectedNode = node;
                _treeView.AfterSelect += handlerAfterSelect;

                // The (IBindingList) has changed,

                // so wire up the child lists to the handler

                while (node.Nodes.Count > 0)
                {    
                    ((IBindingList)((BoundTreeNode)
                       node.Nodes[0]).CurrencyManager.List).ListChanged 
                       -= handlerListChanged;
                    ((IBindingList)((BoundTreeNode)
                       node.Nodes[0]).CurrencyManager.List).ListChanged 
                       += handlerListChanged;
                    node = node.Nodes[0];
                }

            }
        }
    }
}

Last bit of news... We found that changes took place in the IBindingLists; therefore, we have to locate the node in the tree and change the Text property in case the Display Member was the column that was changed. Simply cast the sender to a DataView, get the DataRowView affected by the NewIndex, determine the TableBinding, and search the tree.

// Some data in the lists has changed,

// we may need to update the TreeView Display

private void cm_ListChanged(object sender, ListChangedEventArgs e)
{
    // Cast the sender to a DataView

    DataView dv = (DataView)sender;

    // Get the DataRowView of the newly selected row in the "list".

    DataRowView drv = (DataRowView)dv[e.NewIndex];
    DataRow dr = drv.Row;

    // Start searching the tree with the base nodes collection

    TreeNodeCollection nodes = _treeView.Nodes;
    TreeNode node = null;
    TableBinding tableBinding;

    // TableBinding tells us what the field in the datarow is that will be

    // compared to the tag value in the node and what the display value is

    tableBinding = GetBinding(dr.Table.TableName);

    // Find the node

    if (tableBinding != null)
    {
        node = SelectNode(dr[tableBinding.ValueMember], nodes);
        node.Text = dr[tableBinding.DisplayMember].ToString();
    }
    else
    {
        node = SelectNode(dr[0], nodes);
        node.Text = dr[0].ToString();
    }

}

Points of Interest

One of the most interesting things I learned here dealt with the IBindingList interface of the CurrencyManager. A CurrencyManager's List property contains a list of the items filtered by the parent row. So when you change selected Order, only the appropriate OrderDetail records are in the list. That makes this list constantly changing, and so wiring up events must occur every time the list is changed (i.e., Created).

This thing isn't very fast, so I wouldn't use it for huge DataSets; however, it seems to work fine for smaller sets of data.

Don't forget the kudos to Duncan McKenzie and LZF!

History

Rev. 1 - Awaiting feedback.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here