樹結構放在 WPF ,有大家熟悉的 TreeView、Menu / MenuItem 等等,自定義的話它是 HierarchicalDataTemplate。

用上 MVVM 模式,視圖與數據分離,意味着你不再需要管 UI ,不用再在 TreeView 內上上下下跑來跑去找控件了。MVVM 不是把樹結構變成不是一顆樹,只是,你操作的,是一個具樹結構的集合而已。我很怕搞 UI,我覺得,這是個解脱,起碼對我是那樣。

我説,如果你發現自己在糾結 TreeView 內怎樣找控件,或者在研究它單一個元素的結構(Grid 包裹着 Border、Border又包裹着 TextBox、最後,哦,找到 TextBox 的 Text 了,手起刀落,改它… 之類),與其糾結下去,不如收手吧,試試用下面方式,你會喜歡的。

我覺得本來 WPF 的設計就是給你這樣用的。

TreeView

使用 WPF + MVVM,特別是當你從 WinForm 轉過來,你需要一個重大的思路改變。UI 是用來「顯示」數據,並非「暫存」數據。它只是個與用户交互的媒介。當對數據操作,你要從數據本身下手,而不是從 UI 找。

wpf 樹虛擬化高效查詢_Text

舉個例子,主菜單,是左側顯示,外層 Expander,裏面的內容每一單元放一個 TreeView,TreeView 內每一項的結構是左邊顯示圖示,右邊顯示文字標題,整項都可以雙擊打開某某功能的界面。這很普通吧。但要求是模塊加載後初始化時可以動態插入項,插入邏輯是提供上一層菜單的標題時,插在它下一級。沒有提供上一級時候,加在最頂層。

不是綁定的話,寫一開始的菜單是很簡單,麻煩在於要開放方法出來,接受上級菜單標題 string、圖示 URI 、標題 string、和需要打開的界面引用。這方法的代碼,需要在菜單結構中找出所謂的上級是哪個項。上級沒有的話,加進去 Expander,有上級就糾結了,要在 TreeView 的結構中找,在 UI 找,這時你必須清楚 TreeView 內項目的結構,比如內容是 Grid 你要在裏面找出 TextBlock 控件的文字是什麼,比較一下,符合時按照已定的結構加 node。

wpf 樹虛擬化高效查詢_MVVM_02

花了點時間,寫完。客户説 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 運行,或者自行修改。當前版本已修正此問題。