Click here to Skip to main content
Click here to Skip to main content

DataBound TreeView Control

, 6 Jan 2005
Rate this:
Please Sign up or sign in to vote.
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

About the Author

Wai Friend

United States United States
No Biography provided

Comments and Discussions

 
GeneralMultiple relations: Showing 2 times the same... PinmemberStuFF mc15-Jun-05 4:45 

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

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

| Advertise | Privacy | Mobile
Web03 | 2.8.140721.1 | Last Updated 7 Jan 2005
Article Copyright 2005 by Wai Friend
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid