樹結構放在 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 運行,或者自行修改。當前版本已修正此問題。