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 DataRelation
s 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 SqlDataAdapter
s 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 DataGrid
s 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)
{
this.sqlDataAdapter3.Fill(ds, "Customers");
this.sqlDataAdapter4.Fill(ds, "Orders");
this.sqlDataAdapter5.Fill(ds, "OrderDetails");
TableBinding[] tableBindings = new TableBinding[] {
new TableBinding("Customers", "CustomerID", "CompanyName"),
new TableBinding("Orders", "OrderID", "OrderID"),
new TableBinding("OrderDetails", "ProductID", "ProductID")};
treeResource.TreeView.HideSelection = false;
treeResource.TreeView.ImageList = this.imageList1;
treeResource.TreeView.ImageIndex = 0;
treeResource.TreeView.SelectedImageIndex = 1;
treeResource.LoadTree(ds, tableBindings);
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 DataGrid
s 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;
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 CurrencyManager
s, 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.
private void tv_AfterSelect(object sender, TreeViewEventArgs e)
{
ArrayList nodeList = new ArrayList();
BoundTreeNode node = (BoundTreeNode)((TreeView)sender).SelectedNode;
nodeList.Add(node);
node = (BoundTreeNode)node.Parent;
while (node != null)
{
nodeList.Add(node);
node = (BoundTreeNode)node.Parent;
}
DisablePositionChanged = true;
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 IBindingList
s are created, new ListChanged
handlers are attached.
private void cm_PositionChanged(object sender, EventArgs e)
{
if (!DisablePositionChanged)
{
CurrencyManager cm = (CurrencyManager)sender;
if (cm.Position >= 0)
{
DataRowView drv = (DataRowView)((DataView)cm.List)[cm.Position];
DataRow dr = drv.Row;
if (dr.RowState != DataRowState.Detached)
{
ArrayList dataRows = new ArrayList();
dataRows.Add(dr);
while (dr.Table.ParentRelations.Count > 0)
{
dr = dr.GetParentRow(dr.Table.ParentRelations[0]);
dataRows.Add(dr);
}
TreeNodeCollection nodes = _treeView.Nodes;
TreeNode node = null;
TableBinding tableBinding;
for (int i = dataRows.Count; i>0; i--)
{
dr = (DataRow)dataRows[i-1];
tableBinding = GetBinding(dr.Table.TableName);
if (tableBinding != null)
node = SelectNode(dr[tableBinding.ValueMember],
nodes);
else
node = SelectNode(dr[0], nodes);
nodes = node.Nodes;
}
_treeView.AfterSelect -= handlerAfterSelect;
_treeView.SelectedNode = node;
_treeView.AfterSelect += handlerAfterSelect;
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 IBindingList
s; 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.
private void cm_ListChanged(object sender, ListChangedEventArgs e)
{
DataView dv = (DataView)sender;
DataRowView drv = (DataRowView)dv[e.NewIndex];
DataRow dr = drv.Row;
TreeNodeCollection nodes = _treeView.Nodes;
TreeNode node = null;
TableBinding tableBinding;
tableBinding = GetBinding(dr.Table.TableName);
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 DataSet
s; 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.