转贴自MS:扩展 TreeView 控件 (2)
添加显示和值成员属性
拥有 DataSource 是实现复杂数据绑定的第一步,但该控件需要了解数据的哪些特定字段或属性将用作显示和值成员。Display 成员将用作树节点的标题,而 Value 成员可通过节点的 Value 属性进行访问。这些属性都是字符串,表示字段或属性名,可以方便地添加到控件中: Private m_ValueMember As String Private m_DisplayMember As String <Category("Data")> _ Public Property ValueMember() As String
Get
Return m_ValueMember
End Get
Set(ByVal Value As String)
m_ValueMember = Value
End Set End Property <Category("Data")> _ Public Property DisplayMember() As String
Get
Return m_DisplayMember
End Get
Set(ByVal Value As String)
m_DisplayMember = Value
End Set End 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.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 在返回对象时忽略属性的基本数据类型,因此在使用返回值前需要对其进行转换。 保持数据绑定控件同步
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 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
在前面的代码片段中,您可能注意到了一个称为 FindFirstLeafNode 的函数,在此我想对其加以简要介绍。在我的 TreeView 中,只有叶节点(分层结构中的最终节点)才与 DataSource 中的项相对应,其他所有节点只用于创建分组结构。如果我要创建一个性能优良的数据绑定控件,便始终需要选择一个与 DataSource 相对应的项,因此每当选择组节点时,我就会找到该组下的第一个叶节点,就好象该节点是当前的选定内容。您可以检查该示例的运行情况,但现在您大可放心地使用它。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 IfEnd Function
设置 CurrencyManager 对象的 Position 属性可使其他控件与当前选定项同步,但是当其他控件的位置发生变化时,CurrencyManager 也产生事件,以便相应地更改选定项。要成为一个优秀的数据绑定组件,所选内容应随着数据源位置的更改而移动,修改某一项的数据时,显示应随之更新。CurrencyManager 引发的事件共有三个:CurrentChanged、ItemChanged 和 PositionChanged。最后一个事件相当简单;CurrencyManager 的用途之一是为数据源维护当前位置指示器,以便多个绑定控件均可以显示同一记录或列表项,只要该位置更改,此事件便会引发。其他两个事件有时会相互重叠,因而区别不太明显。以下分别介绍如何在自定义控件中使用这些事件:PositionChanged 是一个比较简单的事件,此处不再赘述;当您要在复杂数据绑定控件(如 Tree)中调整当前选定项时,请使用该事件。只要修改数据源中的项,ItemChanged 事件就会引发,而 CurrentChanged 只有在当前项被修改时才引发。
在我的 TreeView 中,我发现每当我选择一个新项时,所有三个事件均会引发,因此我决定通过更改当前选定项来处理 PositionChanged 事件,而对另外两项不进行任何处理。.NET Framework 文档(英文)建议将数据源强制转换为 IBindingList(如果数据源支持 IBindingList 的话)并改用 ListChanged 事件,但我未实现此功能。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 IfEnd SubPrivate Overloads Function FindNodeByPosition(ByVal index As Integer) _
As TreeNode Return FindNodeByPosition(index, Me.Nodes)End FunctionPrivate 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 NothingEnd Function将 DataSource 转变为树
编写完数据绑定代码后,我可以继续添加管理分组级别的代码,相应地生成树,然后添加一些自定义事件、方法和属性。 管理组
程序员要配置组集合,就必须创建 AddGroup、RemoveGroup 和 ClearGroups 函数。每当修改组集合时,都必须重新绘制树(以反映新配置),因此我创建了一个通用过程 GroupingChanged,当情况发生变化,需要强制重建树时,它可以由控件中的各种代码调用:Private treeGroups As New ArrayList()Public Sub RemoveGroup(ByVal group As Group) If Not treeGroups.Contains(group) Then
treeGroups.Remove(group)
GroupingChanged() End IfEnd SubPublic Overloads Sub AddGroup(ByVal group As Group) Try
treeGroups.Add(group)
GroupingChanged() Catch End TryEnd SubPublic 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 SubPublic Function GetGroups() As Group() Return CType(treeGroups.ToArray(GetType(Group)), Group())End Function生成树
树的实际重建由一对过程来完成:BuildTree 和 AddNodes。由于这两个过程的代码太长,本文并未全部列出,而是尽量概括它们的行为(当然,如果愿意您可以下载完整的代码)。如前所述,程序员可以通过设置一系列组与该控件进行交互,然后在 BuildTree 中使用这些组来确定如何设置树节点。BuildTree 清除当前节点集合,然后遍历整个数据源来处理第一级分组(本文前面的示例和图解中提到的 Publisher),为每个不同的分组值添加一个节点(使用示例中的数据,为每个 pub_id 值添加一个节点),然后调用 AddNodes 来填充第一级分组下的所有节点。AddNodes 递归调用自身以处理任意多的级数,必要时可添加组节点和叶节点。使用两个基于 TreeNode 的自定义类以区别组节点和叶节点,并为两类节点提供各自相应的属性。 自定义 TreeView 事件
每当选择一个节点时,TreeView 都会引发两个事件:BeforeSelect 和 AfterSelect。但在我的控件中,我想使组节点和叶节点的事件不同,于是便添加了自己的事件 BeforeGroupSelect/AfterGroupSelect 和 BeforeLeafSelect/AfterLeafSelect,除基本事件外,还引发了自定义事件参数类:Public Event BeforeGroupSelect _ (ByVal sender As Object, ByVal e As groupTreeViewCancelEventArgs)Public Event AfterGroupSelect _ (ByVal sender As Object, ByVal e As groupTreeViewEventArgs)Public Event BeforeLeafSelect _ (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 SubProtected 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
自定义节点类(TreeLeafNode 和 TreeGroupNode)和自定义事件参数类均包括在中。 示例应用程序
要全面理解本示例控件中的所有代码,您应该了解它在应用程序中的运行情况。包含的示例应用程序使用 pubs.mdb Access 数据库,并说明 Tree 控件如何与其他数据绑定控件一起创建 Windows 应用程序。本例中,尤其值得注意的主要功能包括树与其他绑定控件的同步以及对数据源执行搜索时树节点的自动选择。 注意:本示例应用程序(名为“TheSample”)包含在本文的中。
图 4:数据绑定 TreeView 的演示应用程序 小结
本文介绍的数据绑定 Tree 控件并非适用于所有需要 Tree 控件来显示数据库信息的项目,但它确实介绍了一种可针对个人目的自定义该控件的方法。请记住,您要生成的任何复杂数据绑定控件与 Tree 控件的大部分代码基本相同,您可以通过修改现有代码来简化以后的控件开发过程。
在下一个示例 Drawing Your Own Controls Using GDI+(英文)中,您将看到在不需要使用特定基类(就象我在该控件中继承了 TreeView 控件一样)的情况下,实现数据绑定有一个更容易的方法。