WPF TreeView 支持多选
WPF 控件多选问题
用过的 WPF 的同学,肯定用过控件的选择功能,例如 ListBox 或者 DataGrid 等。其中有一种控件 ———— TreeView 的多选并没有我们想象中的那么开箱即用。
之前就遇到一个需求,TreeView 要支持多个选中项,且能从数据端(ViewModel)改变树节点的选中状态,然而原生 TreeView 控件是不支持设置多个选中项的。
为什么 TreeView 不支持多选呢?
从 TreeView 这个类可以看出,它是继承于 ItemsControl 的,本质上它就是一个嵌套 ItemsControl 的控件。单个 ItemsControl 的选中非常容易处理,多层嵌套就不一样了,原生控件也没有提供直接可用的接口来实现多选项。
失败的尝试
刚开始做多选 TreeView 的时候,首先采用了增加 IsSelected 属性来绑定选中状态,数据结构如下:
public class TreeNode
{
public bool Selected { get; set; }
// other data of tree node...
}
本以为将 TreeViewItem 的 IsSelected 属性绑定到 Selected 属性,再使用 DataTrigger 让选中项样式高亮使即可轻松搞定,没想到写完后测试却发现不同层级的 TreeViewItem 无法同时处于选中状态。因为双向绑定的存在,控件的 IsSelected 属性实时同步到了 ViewModel 的状态数据中,导致多选失败。
后来在 Selected 的 set 方法中尝试加入一些条件判断,试图区分用户的点击和控件自动的数据修改,均没有理想的解决所有场景下的操作要求。
改变思路
上面的方法中,我花了大量的精力去解决控件 IsSelected 属性与 ViewModel 数据的同步,双向绑定使得数据流的管理极为混乱和复杂,那为什么不把两者的关系解耦呢?
- 取消 IsSelected 与 Selected 的绑定,只保留 Selected 数据到控件样式的单向绑定。
- 从 View 的操作出发,将用户的点击事件发送到 ViewModel,ViewModel 可以查到 Ctrl/Shift 按键的状态,此时更新 Selected 将变得非常简单且易维护。通过统一的入口去修改 Selected 属性,而控件的选中状态只由 Selected 数据改变,不随着用户在 View 层的操作而改变。这样的实现方式将极大地怎加选中操作的灵活性。
- 同时,全选、反选、批量选择等操作也能在 ViewModel 中完成,只要思考业务需求而不必考虑控件交互带来的复杂度。
总结
WPF 的双向绑定带给 UI 开发带来极大的便利,却也造成了复杂交互的维护性问题,适当使用单向绑定能有不错的效果。
当时在解决 TreeView 多选问题的时候,我还没有接触 Web 前端。后来学习 React 的时候,发现我在处理 TreeView 数据流动方向的方式竟然与 React 渲染时的单向数据流有一定的相似之处。