图 1:在 TreeView 中显示数据
在本例中,我将创建一个 TreeView 控件,在该控件中可传递一个平面数据集(如图 2 所示),并可轻松地生成图 1 所示的结果。
图 2:平面结果集,包含创建图 1 所示的树所需的所有信息
在开始编码之前,我为新控件想出了一个可以处理该特定数据集的设计,并希望它能够适用于许多其他类似的情形。添加一个足可以使用大多数平面数据创建分层结构的组集合,在该集合中为每一级分层均指定一个分组字段、显示字段和值字段(任一或所有字段均应相同)。为了将图 2 所示的数据转变成图 1 所示的 TreeView,我的新控件要求您定义两个分组级别 Publisher 和 Title,并将 pub_id 定义为 Publisher 组的分组字段,将 title_id 定义为 Title 组的分组字段。除分组字段以外,还需要为每个组指定显示和值字段,以确定在相应组节点上显示的文本以及用来唯一标识特定组的值。当遇到此类数据时,请使用 pub_name/pub_id 和 title/title_id 作为这两个组的显示/值字段。作者信息将变成树的叶节点(分组分层结构末端的节点),您还需要为这些节点指定 ID (au_id) 和显示 (au_lname) 字段。
构建自定义控件时,在开始编码之前确定程序员对该控件的使用方法将有助于提高控件的使用效率。这种情况下,我希望程序员(在给定了前面所示的数据和所需结果的情况下)能够使用如下几行代码完成分组:
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
注意:这并不是我最终编写的代码行,但两者相差不多。在开发控件的过程中,我意识到需要将与 TreeView 关联的 ImageList 中的图像索引与每个分组级别相关联,因此必须向 AddGroup 方法中额外添加一个参数。
为了真正构建该树,我将浏览数据并查找字段(指定为每个分组的分组值)的更改,同时在必要时创建新分组节点,并针对每个数据项创建一个叶节点。由于存在分组节点,因此总节点数将大于数据源中的项目数,但基础数据中的每个项有且仅有一个叶节点。
图 3:分组节点与叶节点
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/bianchengyuyan/)叶节点和分组节点之间的区别(如图 3 所示)对本文的余下部分具有重要意义。我决定将这两类节点区别对待,为每一类节点分别创建自定义节点,并根据所选的节点类型引发不同的事件。
实现数据绑定
为该控件编写代码的第一步是创建项目和相应的起始类。在本例中,我首先创建一个新 Windows 控件库,然后删除默认的 UserControl 类,并用一个从 TreeView 控件继承的新类来代替它:
Public Class dbTreeControl
Inherits System.Windows.Forms.TreeView
从这时起,我将设计一个可以放入到窗体中的控件,并使其具有常规的 TreeView 的外观和功能。下一步是开始添加旨在处理在 TreeView 中加入的新功能所需的代码,即数据绑定和分组数据。
添加 DataSource 属性
我的新控件的所有功能都很重要,但构建复杂数据绑定控件的两个关键问题是处理 DataSource 属性和从数据源的每个对象中检索单个项目。
创建属性例程
首先,任何用于实现复杂数据绑定的控件都需要实现一个 DataSource 属性例程,并保持适当的成员变量:
Private m_DataSource As Object_Public Property DataSource() As ObjectGetReturn m_DataSourceEnd GetSet(ByVal Value As Object)If Value Is Nothing Thencm = NothingGroupingChanged()ElseIf Not (TypeOf Value Is IList Or _TypeOf Value Is IListSource) Then' 不是针对该用途的有效数据源Throw New System.Exception("无效 DataSource")ElseIf TypeOf Value Is IListSource ThenDim myListSource As IListSourcemyListSource = CType(Value, IListSource)If myListSource.ContainsListCollection = True ThenThrow New System.Exception("无效 DataSource")Else' 对,对。它是有效的数据源m_DataSource = Valuecm = CType(Me.BindingContext(Value), _CurrencyManager)GroupingChanged()End IfElsem_DataSource = Valuecm = CType(Me.BindingContext(Value), _CurrencyManager)GroupingChanged()End IfEnd IfEnd IfEnd SetEnd PropertyIList 接口
可用作复杂数据绑定数据源的对象通常都支持,该接口将数据公开为对象集合,并提供若干有用属性,如 Count。我的新 TreeView 控件要求在其绑定中使用一个支持 IList 的对象,但使用另一个接口也可以,因为它提供了一个获取 IList 对象的简便方法 (GetList)。当设置 DataSource 属性后,我首先确定是否提供了有效的对象,即一个支持 IList 或 IListSource 的对象。我真正想要的是 IList,因此如果对象仅支持 IListSource(例如 DataTable),那么我将使用该接口的 GetList() 方法获得正确的对象。
某些实现 IListSource 的对象(如 DataSet)实际上包含多个由 ContainsListCollection 属性表示的列表。如果该属性为 True,则 GetList 将返回一个表示列表(包含多个列表)的 IList 对象。在我的示例中,我决定支持直接连接到 IList 对象或仅包含一个 IList 对象的 IListSource 对象,并忽略需要附加工作来指定数据源的对象,如 DataSet。
注意:如果要支持此类对象(DataSet 或与之类似的对象),您可以再添加一个属性(如 DataMember)来指定用于绑定的特定子列表。如果提供的数据源有效,则最终结果是创建的实例 (cm = Me.BindingContext(Value))。由于该实例将用于访问基础数据源、对象属性和位置信息,因此被存储在局部变量中。
添加显示和值成员属性
拥有 DataSource 是实现复杂数据绑定的第一步,但该控件需要了解数据的哪些特定字段或属性将用作显示和值成员。Display 成员将用作树节点的标题,而 Value 成员可通过节点的 Value 属性进行访问。这些属性都是字符串,表示字段或属性名,可以方便地添加到控件中:
Private m_ValueMember As StringPrivate m_DisplayMember As String_Public Property ValueMember() As StringGetReturn m_ValueMemberEnd GetSet(ByVal Value As String)m_ValueMember = ValueEnd SetEnd Property_Public Property DisplayMember() As StringGetReturn m_DisplayMemberEnd GetSet(ByVal Value As String)m_DisplayMember = ValueEnd SetEnd Property
在此 TreeView 中,这些属性将仅表示叶节点的 Display 和 Value 成员,每个分组级别的相应信息将在 AddGroup 方法中指定。
使用 CurrencyManager 对象
在前面探讨的 DataSource 属性中,创建了一个 CurrencyManager 类的实例,并存储在类级别变量中。通过该对象访问的 CurrencyManager 类是实现数据绑定的关键部分,因为它具有的属性、方法和事件可实现以下功能:
访问数据源的基础 IList 对象 在数据源中检索和设置对象字段或属性,以及 使您的控件与同一窗体中的其他数据绑定控件同步。检索属性/字段值
CurrencyManager 对象允许您通过它的 GetItemProperties 方法从数据源的单个项中检索属性或字段值,如 DisplayMember 或 ValueMember 字段的值。然后使用 PropertyDescriptor 对象获取特定列表项上的特定字段或属性的值。下面的代码片断显示了这些 PropertyDescriptor 对象的创建方法以及如何使用 GetValue 函数获取基础数据源中某一项的属性值。请注意 CurrencyManager 对象的 List 属性:通过它可以访问该控件绑定到的 IList 实例:
Dim myNewLeafNode As TreeLeafNodeDim currObject As ObjectcurrObject = cm.List(currentListIndex)If Me.DisplayMember "" AndAlso Me.ValueMember "" Then' 添加叶节点?Dim pdValue As System.ComponentModel.PropertyDescriptorDim pdDisplay As System.ComponentModel.PropertyDescriptorpdValue = cm.GetItemProperties()(Me.ValueMember)pdDisplay = cm.GetItemProperties()(Me.DisplayMember)myNewLeafNode = _New TreeLeafNode(CStr(pdDisplay.GetValue(currObject)), _currObject, _pdValue.GetValue(currObject), _currentListIndex)
GetValue 在返回对象时忽略属性的基本数据类型,因此在使用返回值前需要对其进行转换。
保持数据绑定控件同步
CurrencyManager 还有一个主要功能:除了可以访问绑定数据源和项属性外,它还允许使用相同的 DataSource 来协调该控件和任何其他控件之间的数据绑定。该支持可用于确保多个同时绑定到同一数据源的控件停留在数据源的同一项。对于我的控件而言,我想确保在树中选择项时,其他所有绑定到同一数据源的控件均指向同一项(同一记录、行、甚至数组,如果您愿意从数据库的角度进行思考)。为此,我覆盖了基本 TreeView 中的 OnAfterSelect 方法。在该方法(在选择树节点后被调用)中,我将 CurrencyManager 对象的 Position 属性设置为当前选定项的索引。与该 TreeView 控件一起提供的示例应用程序阐释了同步控件如何使生成数据绑定用户界面变得更为容易。为了使确定当前选定项的列表位置更为容易,我使用了自定义 TreeNode 类(TreeLeafNode 或 TreeGroupNode),并将每个节点的列表索引存储到创建的 Position 属性中:
Protected Overrides Sub OnAfterSelect _(ByVal e As System.Windows.Forms.TreeViewEventArgs)Dim tln As TreeLeafNodeIf TypeOf e.Node Is TreeGroupNode Thentln = FindFirstLeafNode(e.Node)Dim groupArgs As New groupTreeViewEventArgs(e)RaiseEvent AfterGroupSelect(groupArgs)ElseIf TypeOf e.Node Is TreeLeafNode ThenDim leafArgs As New leafTreeViewEventArgs(e)RaiseEvent AfterLeafSelect(leafArgs)tln = CType(e.Node, TreeLeafNode)End IfIf Not tln Is Nothing ThenIf cm.Position tln.Position Thencm.Position = tln.PositionEnd IfEnd IfMyBase.OnAfterSelect(e)End Sub
在前面的代码片段中,您可能注意到了一个称为 FindFirstLeafNode 的函数,在此我想对其加以简要介绍。在我的 TreeView 中,只有叶节点(分层结构中的最终节点)才与 DataSource 中的项相对应,其他所有节点只用于创建分组结构。如果我要创建一个性能优良的数据绑定控件,便始终需要选择一个与 DataSource 相对应的项,因此每当选择组节点时,我就会找到该组下的第一个叶节点,就好象该节点是当前的选定内容。您可以检查该示例的运行情况,但现在您大可放心地使用它。
Private Function FindFirstLeafNode(ByVal currNode As TreeNode) _As TreeLeafNodeIf TypeOf currNode Is TreeLeafNode ThenReturn CType(currNode, TreeLeafNode)ElseIf currNode.Nodes.Count 0 ThenReturn FindFirstLeafNode(currNode.Nodes(0))ElseReturn NothingEnd IfEnd IfEnd Function
设置 CurrencyManager 对象的 Position 属性可使其他控件与当前选定项同步,但是当其他控件的位置发生变化时,CurrencyManager 也产生事件,以便相应地更改选定项。要成为一个优秀的数据绑定组件,所选内容应随着数据源位置的更改而移动,修改某一项的数据时,显示应随之更新。CurrencyManager 引发的事件共有三个:CurrentChanged、ItemChanged 和 PositionChanged。最后一个事件相当简单;CurrencyManager 的用途之一是为数据源维护当前位置指示器,以便多个绑定控件均可以显示同一记录或列表项,只要该位置更改,此事件便会引发。其他两个事件有时会相互重叠,因而区别不太明显。以下分别介绍如何在自定义控件中使用这些事件:PositionChanged 是一个比较简单的事件,此处不再赘述;当您要在复杂数据绑定控件(如 Tree)中调整当前选定项时,请使用该事件。只要修改数据源中的项,ItemChanged 事件就会引发,而 CurrentChanged 只有在当前项被修改时才引发。
在我的 TreeView 中,我发现每当我选择一个新项时,所有三个事件均会引发,因此我决定通过更改当前选定项来处理 PositionChanged 事件,而对另外两项不进行任何处理。建议将数据源强制转换为 IBindingList(如果数据源支持 IBindingList 的话)并改用 ListChanged 事件,但我未实现此功能。
Private Sub cm_PositionChanged(ByVal sender As Object, _ByVal e As System.EventArgs) Handles cm.PositionChangedDim tln As TreeLeafNodeIf TypeOf Me.SelectedNode Is TreeLeafNode Thentln = CType(Me.SelectedNode, TreeLeafNode)Elsetln = FindFirstLeafNode(Me.SelectedNode)End IfIf tln.Position cm.Position ThenMe.SelectedNode = FindNodeByPosition(cm.Position)End IfEnd SubPrivate Overloads Function FindNodeByPosition(ByVal index As Integer) _As TreeNodeReturn FindNodeByPosition(index, Me.Nodes)End FunctionPrivate Overloads Function FindNodeByPosition(ByVal index As Integer, _ByVal NodesToSearch As TreeNodeCollection) As TreeNodeDim i As Integer = 0Dim currNode As TreeNodeDim tln As TreeLeafNodeDo While i NodesToSearch.CountcurrNode = NodesToSearch(i)i += 1If TypeOf currNode Is TreeLeafNode Thentln = CType(currNode, TreeLeafNode)If tln.Position = index ThenReturn currNodeEnd IfElsecurrNode = FindNodeByPosition(index, currNode.Nodes)If Not currNode Is Nothing ThenReturn currNodeEnd IfEnd IfLoopReturn NothingEnd Function