C# WPF MVVM 实战 – 3 – 树结构
树结构放在 WPF ,有大家熟悉的 TreeView、Menu / MenuItem 等等,自定义的话它是 HierarchicalDataTemplate。
用上 MVVM 模式,视图与数据分离,意味着你不再需要管 UI ,不用再在 TreeView 内上上下下跑来跑去找控件了。MVVM 不是把树结构变成不是一颗树,只是,你操作的,是一个具树结构的集合而已。我很怕搞 UI,我觉得,这是个解脱,起码对我是那样。
我说,如果你发现自己在纠结 TreeView 内怎样找控件,或者在研究它单一个元素的结构(Grid 包裹着 Border、Border又包裹着 TextBox、最后,哦,找到 TextBox 的 Text 了,手起刀落,改它… 之类),与其纠结下去,不如收手吧,试试用下面方式,你会喜欢的。
我觉得本来 WPF 的设计就是给你这样用的。
TreeView
使用 WPF + MVVM,特别是当你从 WinForm 转过来,你需要一个重大的思路改变。UI 是用来「显示」数据,并非「暂存」数据。它只是个与用户交互的媒介。当对数据操作,你要从数据本身下手,而不是从 UI 找。
举个例子,主菜单,是左侧显示,外层 Expander,里面的内容每一单元放一个 TreeView,TreeView 内每一项的结构是左边显示图示,右边显示文字标题,整项都可以双击打开某某功能的界面。这很普通吧。但要求是模块加载后初始化时可以动态插入项,插入逻辑是提供上一层菜单的标题时,插在它下一级。没有提供上一级时候,加在最顶层。
不是绑定的话,写一开始的菜单是很简单,麻烦在于要开放方法出来,接受上级菜单标题 string、图示 URI 、标题 string、和需要打开的界面引用。这方法的代码,需要在菜单结构中找出所谓的上级是哪个项。上级没有的话,加进去 Expander,有上级就纠结了,要在 TreeView 的结构中找,在 UI 找,这时你必须清楚 TreeView 内项目的结构,比如内容是 Grid 你要在里面找出 TextBlock 控件的文字是什么,比较一下,符合时按照已定的结构加 node。
花了点时间,写完。客户说 Expander 不好用,通通改为 TreeView,你懂的。另一个客户,说除了左侧菜单外,希望上面有些传统菜单,额,你又改。设计师哪天看到 Dev 说好,我们改吧,那你又改吧。。。
这些问题,源于算法与 UI 结构紧扣在一起,特别是 XAML 界面,你要多复杂,有多复杂,然后你的插入算法也跟着复杂。而且 UI 变,你也要改。但这世界可以更美好的。
数据结构
为求简单,这结构只有标题。
public class MyMenuItem : INotifyPropertyChanged { public MyMenuItem() { Childs =new ObservableCollection<MyMenuItem>(); } private string text; public string Text { get { return text; } set { text = value; if (PropertyChanged !=null) PropertyChanged(this, new PropertyChangedEventArgs("Text")); } } private ObservableCollection<MyMenuItem> childs; public ObservableCollection<MyMenuItem> Childs { get { return childs; } set { childs = value; if (PropertyChanged !=null) PropertyChanged(this, new PropertyChangedEventArgs("Childs")); } } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion }
数据结构是树结构,你就要把它写成树结构,不用考虑 UI 那边怎样。
要对于结构操作,搜索标题然后加项的话,这类菜单我选择 Breadth First。扩展方法有时候觉得用的机会不多吧,来一个玩玩看。
internal static class MenuItemExtension { internal static MyMenuItem Search( this MyMenuItem node, Predicate<MyMenuItem> match) { Queue<MyMenuItem> queue =new Queue<MyMenuItem>(); queue.Enqueue(node); while (queue.Count >0) { MyMenuItem thisNode = queue.Dequeue(); if (match(thisNode)) return thisNode; foreach (MyMenuItem child in thisNode.Childs) queue.Enqueue(child); } return null; } }
实际插菜单,对外开放的加菜单功能,大概这样实现咯。
public class MenuService { private MyMenuItem MainMenu; public MenuService() { //... 一些拿到主菜单的代码,比如从容器中 Resolve } public void Add(string MenuText, string ParentText) { if (ParentText ==null) { this.MainMenu.Childs.Add(new MyMenuItem { Text = MenuText }); } else { MyMenuItem result =this.MainMenu.Search(x => { return x.Text == ParentText; }); if (result !=null) { result.Childs.Add(new MyMenuItem { Text = MenuText }); } else { throw new ArgumentOutOfRangeException("ParentText"); } } } }
一切都很合理,没有了奇怪的 UI 结构在算法内,任何形式的菜单,都能用这结构和方法。喜欢直接 TreeView 的就 TreeView,复杂起来的界面用 HierarchicalDataTemplate。
绑定写法请自己查 MSDN 或看书,不写出来了。下面源码有些超简单示例。
点击下载源代码:Lepton_Practical_MVVM_3.zip
MVVM 大神 Josh Smith 在 Code Project 写了一篇相当经典的,关于 MVVM 与 TreeView 的做法,点击这里打开。我极力推荐。学习 WPF 和 Silverlight 的同学们,Josh Smith 在 wordpress 写了些博文,应该一篇不漏的看一遍(貌似要FQ)。
后记:2012-09-27 10:35 PM 关于Lepton_Practical_MVVM_3.zip,不好意思我上网抄了个 ViewModelBase 后,心痒,把它的 DisplayName 属性删除后忘记了改 #IF DEBUG 内的代码,请在这后记编写前下载了代码的朋友,在 VS 用 Release Build 运行,或者自行修改。当前版本已修正此问题。