65.9K
CodeProject 正在变化。 阅读更多。
Home

C#/VB.Net 高级WPF TreeView 第 n 部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2018 年 1 月 11 日

CPOL

7分钟阅读

viewsIcon

37353

downloadIcon

988

在WPF TreeView中使用复选框的技巧与窍门。

引言

本文介绍了一种在WPF TreeView的每个树项中显示和管理复选框的解决方案。该解决方案与Josh Smith在2008年提出的文章和解决方案密切相关[1](近10年前)。如今,WPF的世界已经有所发展,并且由于Josh Smith的贡献和发现,我们可以以稍有不同的方式实现一些细节。本文旨在描述一个更新版本,并配以视觉辅助,以帮助新手更好地理解解决方案的工作原理。

背景

CodeProject的一位读者要求提供一个VB.Net解决方案,展示如何使用复选框来管理WPF Tree View中的项目。我找到了Josh Smith的解决方案,并且觉得它没什么问题,只是它不是VB.Net的,并且有一些细节我想稍微改变一下描述方式,以帮助新手理解这个概念。

下面的截图让您对我们在此要解决的用例有所了解。应用程序启动时第一个项目被选中,您可以使用键盘导航树,并使用空格键或回车键选中或取消选中项目。

Demo Screenshot

下面的下一组截图显示了当用户选中相应项目(应用程序启动后)时,TreeView中所有项目的选中状态。

Demo Screenshot Demo Screenshot Demo Screenshot
选中根项目
(武器)
选中第二级项目
(车辆)
选中第三级项目
(潜水艇)

我们可以看到,选中一个节点不仅会影响该节点的状态,还会影响其父节点和子节点的状态。以下是Josh Smith[1]用于在文本中定义此行为的需求列表:

需求 1:树中的每个项目都必须显示一个复选框,该复选框显示底层数据对象的文本和选中状态。

需求 2:当一个项目被选中或取消选中时,其所有子项目应分别被选中或取消选中。

需求 3:如果一个项目的后代没有全部具有相同的选中状态,则该项目必须处于“不确定”状态。

需求 4:从项目到项目的导航只需要按一次箭头键。

需求 5:按下空格键或回车键应切换所选项目的选中状态。

需求 6:单击项目上的复选框应切换其选中状态,但不选中该项目。

需求 7:单击项目上的显示文本应选中该项目,但不切换其选中状态。

需求 8:树中的所有项目默认应处于展开状态。

仔细阅读这些需求(2和3)表明,我们需要访问更多的树节点,以确保所有细节都得到满足。也就是说,所请求的行为需要一种导航概念,该概念不仅像前面指出的那样导航当前节点及其子节点,还导航树项的父节点[4]

上面的动画展示了一个TreeView,在帧0中没有项目被选中(黑色 -> 未选中)。用户单击节点b,系统在帧1中访问节点b以设置选中状态(绿色 -> 选中)。随后的帧2-6显示了系统如何访问每个子节点并设置选中状态。

帧7中的图像显示,系统还会访问已选中项的父项(在本例中是根项a),以验证父项的状态是否也与新选中项b的状态一致。通过查看帧8-9中项a的子项cd,可以确定此问题的答案。

a的正确状态是“不确定”(黄色),因为其一些子项已被选中(绿色),而另一些子项则未被选中(黑色)。由于项a没有父节点,并且cd节点的状态没有改变,因此算法可以在此处(帧11)结束。

如果项a有父节点,算法将不得不访问更多的父节点。但在此情况下,永远不需要访问节点cd的子节点,因为cd不会改变状态。

上述算法用于处理项x的选中操作。不需要对项x的子项进行层序遍历,因为它没有子项。但是,仍然需要遍历父项,并将项ab的状态确定为“不确定”,因为它们的子项具有不同的(选中和未选中)状态。

现在我们可以总结出所需的算法需要实现:

  1. 一个层序遍历算法,从被选中/取消选中的节点开始(选中或取消选中选定节点及其所有子节点),
  2. 并访问选定节点的所有父节点,以根据每个父节点子节点组合的状态重新评估它们的状态。

现在让我们转向代码,研究一个可能的WPF实现来应对这个挑战。

使用代码

与往常一样,C#和VB.Net中提供的示例是通过Window1对象实例化的。请查看代码后台,了解如何创建AppViewModel对象并将其附加到Window1DataContext属性。此机制将Window1 XAML文件中的所有其他绑定连接到AppViewModel对象的属性。这包括包含在AppViewModel对象中实例化并公开的对象中的属性(例如,Root属性绑定到Window1的TreeView的ItemsSource)。

此绑定过程甚至会进一步级联到用于实例化和绑定每个TreeView项的HierarchicalDataTemplate。当然,只有当FooViewModel类中的Children集合包含实际对象时,才会实例化一个TreeView项。

另一个很好的概览是所附项目的类图。该图可以使用Visual Studio生成,并显示驱动应用程序的ViewModels、视图(Window1)和interfaces

Class Diagram

AppViewModel中的CheckItemCommand实现了前面提到的对选定子项及其父项的遍历算法。对Window1.xmal的检查表明,CheckItemCommand绑定到HierarchicalDataTemplate中的CheckBoxVirtualToggleButton附加行为类。这就是CheckBox如何直接通过鼠标转发被选中或取消选中的事件,而VirtualToggleButton类将来自键盘的相同事件中继到AppViewModel对象。

AppViewModel.CheckItemCommand调用以下代码来处理复选框被切换的事件:

private void CheckItemCommand_Executed(IFooViewModel changedItem)
{
    var items = TreeLib.BreadthFirst.Traverse.LevelOrder
                  <IFooViewModel>(changedItem.Children, i => i.Children);

    // All children of the checked/unchecked item have to assume it's state
    foreach (var item in items)
    {
        var node = item.Node as FooViewModel;
        node.IsChecked = changedItem.IsChecked;
    }
    
    // Visit each parent in turn and determine their correct states
    var parentItem = changedItem.Parent;
    
    for( ; parentItem != null; parentItem = parentItem.Parent)
    {
      ResetParentItemState(parentItem as IFooViewModel);
    }
}

private void ResetParentItemState(IFooViewModel item)
{
    if (item == null)
      return;
    
    if (item.ChildrenCount == 0)
      return;

    var itemChildren = item.Children.ToArray();

    bool? firstChild = itemChildren[0].IsChecked;
    
    for(int i=1; i< itemChildren.Length; i++)
    {
      if (Object.Equals(firstChild, itemChildren[i].IsChecked) == false)
      {
        // Two different child states found for this parent item ...
        item.IsChecked = null;
        return;
      }
    }
    
    // All child items have the same state as the first child
    item.IsChecked = firstChild;
}
Private Sub CheckItemCommand_Executed(ChangedItem As IFooViewModel)
    Dim items = TreeLib.BreadthFirst.Traverse.LevelOrder(Of IFooViewModel)(ChangedItem.Children, Function(i) i.Children)

    '' All children of the checked/unchecked item have to assume it's state
    For Each item In items
        Dim node = TryCast(item.Node, FooViewModel)
        node.IsChecked = ChangedItem.IsChecked
    Next

    '' Visit each parent in turn And determine their correct states
    Dim parentItem = ChangedItem.Parent

    While parentItem IsNot Nothing
        ResetParentItemState(TryCast(parentItem, IFooViewModel))

        parentItem = parentItem.Parent
    End While
End Sub

Private Sub ResetParentItemState(item As IFooViewModel)

    If (item Is Nothing) Then
        Return
    End If

    If item.ChildrenCount = 0 Then
        Return
    End If

    Dim itemChildren = item.Children.ToArray()

    Dim firstChild As Boolean?
    firstChild = itemChildren(0).IsChecked

    For i = 0 To itemChildren.Length - 1
        If (Object.Equals(firstChild, itemChildren(i).IsChecked) = False) Then

            '' Two different child states found for this parent item ...
            item.IsChecked = Nothing
            Return
        End If
    Next

    '' All child items have the same state as the first child
    item.IsChecked = firstChild
End Sub

CheckItemCommand_Executed方法中的上述代码将以ChangedItem参数的形式调用,该参数代表刚刚被切换复选框的项。此代码实现了前面[4]所述的通过TreeLib库对被选中/取消选中项进行的层序遍历。后面的循环和ResetParentItemState方法的调用实现了对所有父项的重新评估和遍历。

两种交互模式,CheckBoxVirtualToggleButton附加行为类,都调用相同的代码,这使得核心操作非常简单。

结论

本文中的代码是Josh Smith另一篇精彩文章的更新版本。我希望这些额外的动画工作和略有不同的实现,能够帮助那些还在为MVVM/WPF(相对于TreeView控件)这个非平凡的概念感到困惑的开发者。

请仔细查看源代码,因为我尝试在几乎所有地方都包含了注释。

您还有问题吗?那就不要犹豫,告诉我您的反馈。

参考文献

© . All rights reserved.