This article focuses on how to use the TreeView control. The TreeView control has rich functions and can be used in many situations. Summary: Describes how to add data binding functionality to the TreeView control. It is one of a series of Microsoft Windows control development examples. You can read this article in conjunction with the related overview article.
Introduction
Where possible, you should start with off-the-shelf controls; because the Microsoft® Windows® Forms controls provided include so much coding and testing that it would be a waste to abandon them and start from scratch. Based on this, in this example, I will inherit an existing Windows Forms control, TreeView, and then customize it. When you download the code for the TreeView control, you also get additional control development examples, as well as a sample application that demonstrates how to use the enhanced TreeView with other data-bound controls.
Designing a data binding tree view
For Windows developers, adding data binding to the TreeView control is a common problem, but because there is one major difference between TreeView and other controls (such as ListBox or DataGrid ) (i.e., TreeView displays hierarchical data), the basic control This feature is not supported yet (that is, we still have to use it). Given a data table, it's clear how to display that information in a ListBox or DataGrid , but using the layered nature of a TreeView to display the same data is less straightforward. Personally, I have applied many different methods when displaying data using TreeView , but there is one method that is most commonly used: grouping the data in the table by certain fields, as shown in Figure 1.
Figure 1: Displaying data in TreeView
In this example, I'll create a TreeView control in which I can pass a flat dataset (shown in Figure 2) and easily produce the results shown in Figure 1.
Figure 2: Flat result set containing all the information needed to create the tree shown in Figure 1
Before I started coding, I came up with a design for the new control that would handle this particular data set, and hopefully it would work for many other similar situations. Add a group collection large enough to create a hierarchical structure using most flat data, in which you specify a grouping field, display field, and value field for each level of hierarchy (any or all fields should be the same). In order to transform the data shown in Figure 2 into the TreeView shown in Figure 1, my new control requires you to define two grouping levels, Publisher and Title, and define pub_id as the grouping field of the Publisher group and title_id as the grouping field of the Title group. Grouping field. In addition to the grouping fields, you also need to specify display and value fields for each group to determine the text that displays on the corresponding group node and the value that uniquely identifies the specific group. When encountering this kind of data, use pub_name/pub_id and title/title_id as the display/value fields for these two groups. The author information becomes the leaf nodes of the tree (the nodes at the end of the grouping hierarchy), and you also need to specify the ID ( au_id ) and display ( au_lname ) fields for these nodes.
When building a custom control, determining how the programmer will use the control before starting coding will help make the control more efficient. In this case, I would like the programmer (given the data shown earlier and the desired result) to be able to accomplish the grouping with a few lines of code like this:
| With DbTreeControl .ValueMember = au_id .DisplayMember = au_lname .DataSource = myDataTable.DefaultView .AddGroup(Publisher, pub_id, pub_name, pub_id) .AddGroup(Title, title_id, title, title_id) End With |
Note: This is not the final line of code I wrote, but it is similar. While developing the control, I realized that I needed to associate the image index in the ImageList associated with the TreeView with each grouping level, so I had to add an extra parameter to the AddGroup method.
To actually build the tree, I'll go through the data and look for changes to the fields (specified as the grouping values for each grouping) while creating new grouping nodes if necessary and a leaf node for each data item. Because of the grouping nodes, the total number of nodes will be greater than the number of items in the data source, but there will be exactly one leaf node for each item in the underlying data.
Figure 3: Group nodes and leaf nodes
The distinction between leaf nodes and group nodes (shown in Figure 3) will be important for the remainder of this article. I decided to treat these two types of nodes differently, create custom nodes for each type of node, and raise different events based on the selected node type.
Implement data binding
The first step in writing code for this control is to create the project and corresponding starting class. In this example, I first create a new Windows Control Library, then delete the default UserControl class and replace it with a new class that inherits from the TreeView control:
Public Class dbTreeControl
Inherits System.Windows.Forms.TreeView
From this point on, I will design a control that can be placed on a form and have the look and functionality of a regular TreeView . The next step is to start adding the code required to handle the new functionality being added to the TreeView , namely data binding and grouping data.
Add DataSource property
All of the functionality of my new control is important, but two key issues in building complex data-bound controls are handling the DataSource properties and retrieving individual items from each object in the data source.
Create attribute routine
First, any control used to implement complex data binding needs to implement a DataSource property routine and maintain appropriate member variables:
| Private m_DataSource As Object _ Public Property DataSource() As Object Get Return m_DataSource End Get Set(ByVal Value As Object) If Value Is Nothing Then cm = Nothing GroupingChanged() Else If Not (TypeOf Value Is IList Or _ TypeOf Value Is IListSource) Then ' is not a valid data source for this purpose Throw New System.Exception(Invalid DataSource) Else If TypeOf Value Is IListSource Then Dim myListSource As IListSource myListSource = CType(Value, IListSource) If myListSource.ContainsListCollection = True Then Throw New System.Exception(Invalid DataSource) Else 'Yes, yes. it is a valid data source m_DataSource = Value cm = CType(Me.BindingContext(Value), _ CurrencyManager) GroupingChanged() End If Else m_DataSource = Value cm = CType(Me.BindingContext(Value), _ CurrencyManager) GroupingChanged() End If End If End If End Set End Property |
Objects that can be used as data sources for complex data bindings are generally supported. This interface exposes the data as a collection of objects and provides several useful properties, such as Count . My new TreeView control requires an IList -backed object in its binding, but using another interface works fine because it provides a convenient way to get an IList object ( GetList ). When setting the DataSource property, I first determine if a valid object is provided, that is, an object that supports IList or IListSource . What I really want is an IList , so if the object only supports IListSource (e.g. DataTable ), then I will use the GetList() method of that interface to get the correct object.
Some objects that implement IListSource (such as DataSet ) actually contain multiple lists represented by the ContainsListCollection property. If this property is True , GetList will return an IList object representing a list (containing multiple lists). In my example, I decided to support direct connections to IList objects or IListSource objects that only contain one IList object, and ignore objects that require additional work to specify a data source, such as a DataSet .
Note: If you want to support such objects ( DataSet or similar), you can add an additional property (such as DataMember ) to specify a specific sublist for binding.
If the supplied data source is valid, the end result is a created instance ( cm = Me.BindingContext(Value) ). Because this instance will be used to access the underlying data source, object properties, and location information, it is stored in local variables.
Add display and value member properties
Having a DataSource is the first step in implementing complex data binding, but the control needs to know which specific fields or properties of the data will be used as display and value members. The Display member will be used as the title of the tree node, while the Value member is accessible through the node's Value property. These properties are all strings representing field or property names and can be easily added to the control:
| Private m_ValueMember As String Private m_DisplayMember As String _ Public Property ValueMember() As String Get Return m_ValueMember End Get Set(ByVal Value As String) m_ValueMember = Value End Set End Property _ Public Property DisplayMember() As String Get Return m_DisplayMember End Get Set(ByVal Value As String) m_DisplayMember = Value End Set End Property |
In this TreeView , these properties will only represent the Display and Value members of the leaf nodes, and the corresponding information for each grouping level will be specified in the AddGroup method.
Using the CurrencyManager object
In the DataSource property discussed earlier, an instance of the CurrencyManager class is created and stored in a class-level variable. The CurrencyManager class accessed through this object is a key part of implementing data binding because it has properties, methods, and events that enable the following functions:
Retrieve attribute/field value
The CurrencyManager object allows you to retrieve property or field values, such as the value of a DisplayMember or ValueMember field, from an individual item in a data source through its GetItemProperties method. Then use a PropertyDescriptor object to get the value of a specific field or property on a specific list item. The following code snippet shows how to create these PropertyDescriptor objects and how to use the GetValue function to obtain the property value of an item in the underlying data source. Note the List property of the CurrencyManager object: it provides access to the IList instance to which the control is bound:
| Dim myNewLeafNode As TreeLeafNode Dim currObject As Object currObject = cm.List(currentListIndex) If Me.DisplayMember <> AndAlso Me.ValueMember <> Then 'Add leaf node? Dim pdValue As System.ComponentModel.PropertyDescriptor Dim pdDisplay As System.ComponentModel.PropertyDescriptor pdValue = cm.GetItemProperties()(Me.ValueMember) pdDisplay = cm.GetItemProperties()(Me.DisplayMember) myNewLeafNode = _ New TreeLeafNode(CStr(pdDisplay.GetValue(currObject)), _ currObject, _ pdValue.GetValue(currObject), _ currentListIndex) |
GetValue ignores the underlying data type of the property when returning an object, so the return value needs to be converted before using it.
Keep data-bound controls in sync
The CurrencyManager has one more major feature: in addition to providing access to bound data sources and item properties, it allows the same DataSource to be used to coordinate data binding between this control and any other control. This support can be used to ensure that multiple controls that are bound to the same data source at the same time stay on the same item of the data source. For my control, I want to make sure that when an item is selected in the tree, all other controls bound to the same data source are pointing to the same item (the same record, row, or even array, if you're willing to think in terms of a database) . To do this, I override the OnAfterSelect method in the base TreeView . In that method (which is called after selecting the tree node), I set the Position property of the CurrencyManager object to the index of the currently selected item. The sample application provided with the TreeView control illustrates how synchronization controls make it easier to build data-bound user interfaces. To make it easier to determine the list position of the currently selected item, I used a custom TreeNode class ( TreeLeafNode or TreeGroupNode ) and stored the list index of each node into the Position property I created:
| Protected Overrides Sub OnAfterSelect _ (ByVal e As System.Windows.Forms.TreeViewEventArgs) Dim tln As TreeLeafNode If TypeOf e.Node Is TreeGroupNode Then tln = FindFirstLeafNode(e.Node) Dim groupArgs As New groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(groupArgs) ElseIf TypeOf e.Node Is TreeLeafNode Then Dim leafArgs As New leafTreeViewEventArgs(e) RaiseEvent AfterLeafSelect(leafArgs) tln = CType(e.Node, TreeLeafNode) End If If Not tln Is Nothing Then If cm.Position <> tln.Position Then cm.Position = tln.Position End If End If MyBase.OnAfterSelect(e) End Sub |
In the previous code snippet, you may have noticed a function called FindFirstLeafNode , which I want to briefly introduce here. In my TreeView , only the leaf nodes (the final nodes in the hierarchy) correspond to items in the DataSource , all other nodes are only used to create the grouping structure. If I want to create a well-performing data-bound control, I always need to select an item corresponding to the DataSource , so whenever I select a group node, I find the first leaf node under the group, as if This node is the current selection. You can check out the example in action, but for now you can feel free to use it.
| Private Function FindFirstLeafNode(ByVal currNode As TreeNode) _ As TreeLeafNode If TypeOf currNode Is TreeLeafNode Then Return CType(currNode, TreeLeafNode) Else If currNode.Nodes.Count > 0 Then Return FindFirstLeafNode(currNode.Nodes(0)) Else Return Nothing End If End If End Function |
Setting the Position property of the CurrencyManager object synchronizes other controls with the current selection, but when the position of other controls changes, the CurrencyManager also generates events so that the selection changes accordingly. To be a good data-bound component, the selection should move as the location of the data source changes, and the display should update when the data for an item is modified. There are three events raised by CurrencyManager : CurrentChanged , ItemChanged and PositionChanged . The last event is fairly simple; one of the purposes of the CurrencyManager is to maintain a current position indicator for the data source so that multiple bound controls can all display the same record or list item, and this event is raised whenever the position changes. The other two events sometimes overlap and the distinction is less clear. The following describes how to use these events in custom controls: PositionChanged is a relatively simple event and will not be described here; when you want to adjust the currently selected item in a complex data-bound control (such as Tree), please use The event. The ItemChanged event is raised whenever an item in the data source is modified, while CurrentChanged is raised only when the current item is modified.
In my TreeView , I found that whenever I selected a new item, all three events were raised, so I decided to handle the PositionChanged event by changing the currently selected item and do nothing with the other two. It is recommended to cast the data source to IBindingList (if the data source supports IBindingList ) and use the ListChanged event instead, but I have not implemented this functionality.
| Private Sub cm_PositionChanged(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles cm.PositionChanged Dim tln As TreeLeafNode If TypeOf Me.SelectedNode Is TreeLeafNode Then tln = CType(Me.SelectedNode, TreeLeafNode) Else tln = FindFirstLeafNode(Me.SelectedNode) End If If tln.Position <> cm.Position Then Me.SelectedNode = FindNodeByPosition(cm.Position) End If End Sub Private Overloads Function FindNodeByPosition(ByVal index As Integer) _ As TreeNode Return FindNodeByPosition(index, Me.Nodes) End Function Private Overloads Function FindNodeByPosition(ByVal index As Integer, _ ByVal NodesToSearch As TreeNodeCollection) As TreeNode Dim i As Integer = 0 Dim currNode As TreeNode Dim tln As TreeLeafNode Do While i < NodesToSearch.Count currNode = NodesToSearch(i) i += 1 If TypeOf currNode Is TreeLeafNode Then tln = CType(currNode, TreeLeafNode) If tln.Position = index Then Return currNode End If Else currNode = FindNodeByPosition(index, currNode.Nodes) If Not currNode Is Nothing Then Return currNode End If End If Loop Return Nothing End Function |
Convert DataSource to tree
After writing the data binding code, I can go ahead and add the code to manage the grouping levels, build the tree accordingly, and then add some custom events, methods, and properties.
Management group
To configure a collection of groups, the programmer must create the AddGroup , RemoveGroup , and ClearGroups functions. Whenever the group collection is modified, the tree must be redrawn (to reflect the new configuration), so I created a generic procedure, GroupingChanged , that can be called by various code in the control when circumstances change and the tree needs to be forced to be rebuilt:
| Private treeGroups As New ArrayList() Public Sub RemoveGroup(ByVal group As Group) If Not treeGroups.Contains(group) Then treeGroups.Remove(group) GroupingChanged() End If End Sub Public Overloads Sub AddGroup(ByVal group As Group) Try treeGroups.Add(group) GroupingChanged() Catch End Try End Sub Public Overloads Sub AddGroup(ByVal name As String, _ ByVal groupBy As String, _ ByVal displayMember As String, _ ByVal valueMember As String, _ ByVal imageIndex As Integer, _ ByVal selectedImageIndex As Integer) Dim myNewGroup As New Group(name, groupBy, _ displayMember, valueMember, _ imageIndex, selectedImageIndex) Me.AddGroup(myNewGroup) End Sub Public Function GetGroups() As Group() Return CType(treeGroups.ToArray(GetType(Group)), Group()) End Function |
spanning tree
The actual reconstruction of the tree is done by a pair of processes: BuildTree and AddNodes . Since the code for these two processes is too long, this article does not list them all, but tries to summarize their behavior (of course, you can download the complete code if you like). As mentioned earlier, programmers can interact with this control by setting up a series of groups, and then using these groups in BuildTree to determine how to set up the tree nodes. BuildTree clears the current node collection and then iterates through the entire data source to process the first level grouping (Publisher mentioned in the example and illustration earlier in this article), adding a node for each different grouping value (using the data in the example, for each Add a node with a pub_id value), and then call AddNodes to populate all nodes under the first-level grouping. AddNodes calls itself recursively to handle as many levels as necessary, adding group nodes and leaf nodes as necessary. Use two custom classes based on TreeNode to distinguish group nodes and leaf nodes, and provide corresponding attributes for the two types of nodes.
Custom TreeView events
Whenever a node is selected, TreeView raises two events: BeforeSelect and AfterSelect . But in my control, I wanted to make the events of group nodes and leaf nodes different, so I added my own events BeforeGroupSelect/AfterGroupSelect and BeforeLeafSelect/AfterLeafSelect . In addition to the basic events, I also triggered a custom event parameter class:
| Public Event BeforeGroupSelect _ (ByVal sender As Object, ByVal e As groupTreeViewCancelEventArgs) Public Event AfterGroupSelect _ (ByVal sender As Object, ByVal e As groupTreeViewEventArgs) Public EventBeforeLeafSelect _ (ByVal sender As Object, ByVal e As leafTreeViewCancelEventArgs) Public Event AfterLeafSelect _ (ByVal sender As Object, ByVal e As leafTreeViewEventArgs) Protected Overrides Sub OnBeforeSelect _ (ByVal e As System.Windows.Forms.TreeViewCancelEventArgs) If TypeOf e.Node Is TreeGroupNode Then Dim groupArgs As New groupTreeViewCancelEventArgs(e) RaiseEvent BeforeGroupSelect(CObj(Me), groupArgs) ElseIf TypeOf e.Node Is TreeLeafNode Then Dim leafArgs As New leafTreeViewCancelEventArgs(e) RaiseEvent BeforeLeafSelect(CObj(Me), leafArgs) End If MyBase.OnBeforeSelect(e) End Sub Protected Overrides Sub OnAfterSelect _ (ByVal e As System.Windows.Forms.TreeViewEventArgs) Dim tln As TreeLeafNode If TypeOf e.Node Is TreeGroupNode Then tln = FindFirstLeafNode(e.Node) Dim groupArgs As New groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(CObj(Me), groupArgs) ElseIf TypeOf e.Node Is TreeLeafNode Then Dim leafArgs As New leafTreeViewEventArgs(e) RaiseEvent AfterLeafSelect(CObj(Me), leafArgs) tln = CType(e.Node, TreeLeafNode) End If If Not tln Is Nothing Then If cm.Position <> tln.Position Then cm.Position = tln.Position End If End If MyBase.OnAfterSelect(e) End Sub |
Custom node classes ( TreeLeafNode and TreeGroupNode ) and custom event parameter classes are included in the downloadable code.
Sample application
To fully understand all the code in this sample control, you should understand how it works in your application. The included sample application uses the pubs.mdb Access database and illustrates how the Tree control can be used with other data-bound controls to create Windows applications. Key features of particular note in this case include the synchronization of the tree with other bound controls and the automatic selection of tree nodes when performing a search on the data source.
Note: This sample application (named TheSample) is included in the download for this article.
Figure 4: Demo application for data-bound TreeView
summary
The data-bound Tree control described in this article is not suitable for every project that requires a Tree control to display database information, but it does introduce a method for customizing the control for personal purposes. Remember that any complex data-bound control you want to build will have much of the same code as the Tree control, and you can simplify future control development by modifying the existing code.