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





5.00/5 (6投票s)
在WPF TreeView中使用复选框的技巧与窍门。
引言
本文介绍了一种在WPF TreeView的每个树项中显示和管理复选框的解决方案。该解决方案与Josh Smith在2008年提出的文章和解决方案密切相关[1](近10年前)。如今,WPF的世界已经有所发展,并且由于Josh Smith的贡献和发现,我们可以以稍有不同的方式实现一些细节。本文旨在描述一个更新版本,并配以视觉辅助,以帮助新手更好地理解解决方案的工作原理。
背景
CodeProject的一位读者要求提供一个VB.Net解决方案,展示如何使用复选框来管理WPF Tree View中的项目。我找到了Josh Smith的解决方案,并且觉得它没什么问题,只是它不是VB.Net的,并且有一些细节我想稍微改变一下描述方式,以帮助新手理解这个概念。
下面的截图让您对我们在此要解决的用例有所了解。应用程序启动时第一个项目被选中,您可以使用键盘导航树,并使用空格键或回车键选中或取消选中项目。
下面的下一组截图显示了当用户选中相应项目(应用程序启动后)时,TreeView中所有项目的选中状态。
![]() | ![]() | ![]() |
选中根项目 (武器) | 选中第二级项目 (车辆) | 选中第三级项目 (潜水艇) |
我们可以看到,选中一个节点不仅会影响该节点的状态,还会影响其父节点和子节点的状态。以下是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的子项c和d,可以确定此问题的答案。
项a的正确状态是“不确定”(黄色),因为其一些子项已被选中(绿色),而另一些子项则未被选中(黑色)。由于项a没有父节点,并且c和d节点的状态没有改变,因此算法可以在此处(帧11)结束。
如果项a有父节点,算法将不得不访问更多的父节点。但在此情况下,永远不需要访问节点c和d的子节点,因为c或d不会改变状态。
上述算法用于处理项x的选中操作。不需要对项x的子项进行层序遍历,因为它没有子项。但是,仍然需要遍历父项,并将项a和b的状态确定为“不确定”,因为它们的子项具有不同的(选中和未选中)状态。
现在我们可以总结出所需的算法需要实现:
- 一个层序遍历算法,从被选中/取消选中的节点开始(选中或取消选中选定节点及其所有子节点),
- 并访问选定节点的所有父节点,以根据每个父节点子节点组合的状态重新评估它们的状态。
现在让我们转向代码,研究一个可能的WPF实现来应对这个挑战。
使用代码
与往常一样,C#和VB.Net中提供的示例是通过Window1
对象实例化的。请查看代码后台,了解如何创建AppViewModel
对象并将其附加到Window1
的DataContext
属性。此机制将Window1
XAML文件中的所有其他绑定连接到AppViewModel
对象的属性。这包括包含在AppViewModel
对象中实例化并公开的对象中的属性(例如,Root属性绑定到Window1
的TreeView的ItemsSource
)。
此绑定过程甚至会进一步级联到用于实例化和绑定每个TreeView项的HierarchicalDataTemplate
。当然,只有当FooViewModel
类中的Children
集合包含实际对象时,才会实例化一个TreeView项。
另一个很好的概览是所附项目的类图。该图可以使用Visual Studio生成,并显示驱动应用程序的ViewModels
、视图(Window1
)和interfaces
。
AppViewModel
中的CheckItemCommand
实现了前面提到的对选定子项及其父项的遍历算法。对Window1.xmal的检查表明,CheckItemCommand
绑定到HierarchicalDataTemplate
中的CheckBox
和VirtualToggleButton
附加行为类。这就是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
方法的调用实现了对所有父项的重新评估和遍历。
两种交互模式,CheckBox
和VirtualToggleButton
附加行为类,都调用相同的代码,这使得核心操作非常简单。
结论
本文中的代码是Josh Smith另一篇精彩文章的更新版本。我希望这些额外的动画工作和略有不同的实现,能够帮助那些还在为MVVM/WPF(相对于TreeView控件)这个非平凡的概念感到困惑的开发者。
请仔细查看源代码,因为我尝试在几乎所有地方都包含了注释。
您还有问题吗?那就不要犹豫,告诉我您的反馈。