1. 程式人生 > 實用技巧 >設計模式-具有Model-View-ViewModel設計模式的WPF應用

設計模式-具有Model-View-ViewModel設計模式的WPF應用

設計模式-具有Model-View-ViewModel設計模式的WPF應用


翻譯自:MSDN雜誌問題/2009年/二月

有許多流行的設計模式可以幫助馴服這種笨拙的野獸,但是要正確地分離和解決眾多問題可能很困難。模式越複雜,以後使用這種快捷方式破壞以前所有以正確方式做事的努力的可能性就越大。

並非總是設計模式有問題。有時,我們使用複雜的設計模式,這需要編寫大量程式碼,因為使用的UI平臺無法很好地適應更簡單的模式。所需要的是一個平臺,該平臺可輕鬆使用經過時間驗證,開發人員認可的簡單設計模式來構建UI。幸運的是,Windows Presentation Foundation(WPF)正是提供了這一點。

隨著軟體界越來越多地採用WPF,WPF社群一直在開發自己的模式和實踐生態系統。在本文中,我將回顧一些使用WPF設計和實現客戶端應用程式的最佳實踐。通過結合使用WPF的某些核心功能和Model-View-View Model(MVVM)設計模式,我將遍歷一個示例程式,該程式演示以“正確的方式”構建WPF應用程式有多麼簡單。

到本文結束時,將很清楚如何將資料模板,命令,資料繫結,資源系統和MVVM模式組合在一起,以建立一個簡單,可測試的健壯框架,任何WPF應用程式都可以在此框架上蓬勃發展。本文隨附的演示程式可以用作使用MVVM作為其核心體系結構的實際WPF應用程式的模板。演示解決方案中的單元測試表明,在一組ViewModel類中存在該功能時,測試該應用程式使用者介面的功能是多麼容易。在深入探討細節之前,讓我們回顧一下為什麼首先應該使用像MVVM這樣的模式。

秩序與混亂

在簡單的“ Hello,World!”程式中使用設計模式是不必要的,而且會適得其反。任何有能力的開發人員一眼就能理解幾行程式碼。但是,隨著程式中功能部件數量的增加,程式碼行和移動部件的數量也相應增加。最終,系統的複雜性及其所包含的反覆出現的問題促使開發人員以一種易於理解,討論,擴充套件和排除故障的方式來組織程式碼。通過將眾所周知的名稱應用於原始碼中的某些實體,我們減少了複雜系統的認知混亂。我們通過考慮程式碼在系統中的功能角色來確定要應用於一段程式碼的名稱。

開發人員經常有意根據設計模式來構造程式碼,而不是讓這些模式有機地出現。兩種方法都沒有錯,但是在本文中,我研究了顯式使用MVVM作為WPF應用程式體系結構的好處。某些類的名稱包含MVVM模式中的知名術語,例如,如果該類是檢視的抽象,則以“ ViewModel”結尾。這種方法有助於避免前面提到的認知混亂。相反,您可以快樂地處於受控的混亂的狀態,這是大多數專業軟體開發專案中的自然狀態!

模型-檢視-檢視模型的演變

自從人們開始建立軟體使用者介面以來,一直存在流行的設計模式來幫助使它變得更容易。例如,Model-View-Presenter(MVP)模式在各種UI程式設計平臺上都非常流行。MVP是Model-View-Controller模式的變體,已經存在了數十年。如果您以前從未使用過MVP模式,則這裡有一個簡化的說明。您在螢幕上看到的是檢視,顯示的資料是模型,演示者將兩者掛鉤。該檢視依賴於Presenter來向其填充模型資料,對使用者輸入做出反應,提供輸入驗證(可能通過委派給模型)以及其他此類任務。如果您想了解有關Model View Presenter的更多資訊,建議您閱讀Jean-Paul Boodhoo的2006年8月“設計模式”專欄。

早在2004年,Martin Fowler就發表了一篇有關模式的文章,該模型名為Presentation Model(PM)。PM模式與MVP相似,因為它將檢視與其行為和狀態分開。PM模式有趣的部分是建立了檢視的抽象,稱為Presentation Model。這樣,檢視僅成為表示模型的呈現。在Fowler的解釋中,他表明表示模型經常更新其檢視,從而使兩者保持同步。該同步邏輯作為程式碼存在於Presentation Model類中。

2005年,John Gossman(目前是Microsoft的WPF和Silverlight架構師之一)在他的部落格中公開了Model-View-ViewModel(MVVM)模式。MVVM與Fowler的Presentation Model相同,因為這兩種模式都具有View的抽象,其中包含View的狀態和行為。Fowler引入了Presentation Model作為建立與UI平臺無關的檢視抽象的一種方式,而Gossman引入了MVVM作為利用WPF核心功能簡化使用者介面建立的標準化方法。從這個意義上講,我認為MVVM是針對WPF和Silverlight平臺量身定製的更為通用的PM模式的專業化。

在2008年9月發行的Glenn Block的優秀文章“稜鏡:使用WPF構建複合應用程式的模式”中,他解釋了針對WPF的Microsoft複合應用程式指南。從未使用術語ViewModel。相反,術語“表示模型”用於描述檢視的抽象。但是,在整篇文章中,我將模式稱為MVVM,將檢視的抽象稱為ViewModel。我發現該術語在WPF和Silverlight社群中更為流行。

與MVP中的Presenter不同,ViewModel不需要引用檢視。檢視繫結到ViewModel的屬性,而ViewModel的屬性又公開了模型物件中包含的資料以及該檢視特定的其他狀態。view和ViewModel之間的繫結很容易構造,因為ViewModel物件被設定為檢視的DataContext。如果ViewModel中的屬性值發生更改,這些新值將通過資料繫結自動傳播到檢視。當用戶單擊檢視中的按鈕時,將執行ViewModel上的命令以執行請求的操作。ViewModel(而不是View)執行對模型資料所做的所有修改。

檢視類不知道存在模型類,而ViewModel和模型不知道檢視。實際上,該模型完全忽略了ViewModel和檢視的存在。您將很快看到,這是一個非常鬆散的耦合設計,它以多種方式帶來收益。

為什麼WPF開發人員喜歡MVVM

一旦開發人員對WPF和MVVM感到滿意,可能很難區分兩者。MVVM是WPF開發人員的通用語,因為它非常適合WPF平臺,並且WPF旨在簡化使用MVVM模式構建應用程式的過程。實際上,Microsoft在內部使用MVVM來開發WPF應用程式,例如Microsoft Expression Blend,而核心WPF平臺正在建設中。WPF的許多方面,例如免看控制模型和資料模板,都充分利用了顯示與MVVM促進的狀態和行為的強烈分離。

WPF使MVVM成為一種絕佳模式的WPF的最重要方面是資料繫結基礎結構。通過將檢視的屬性繫結到ViewModel,可以使兩者之間鬆散耦合,並且完全不需要在直接更新檢視的ViewModel中編寫程式碼。資料繫結系統還支援輸入驗證,這提供了將驗證錯誤傳輸到檢視的標準化方法。

WPF的其他兩個使該模式可用的功能是資料模板和資源系統。資料模板將檢視應用於使用者介面中顯示的ViewModel物件。您可以在XAML中宣告模板,然後讓資源系統在執行時自動為您查詢和應用這些模板。您可以在我2008年7月的文章“資料和WPF:使用資料繫結和WPF自定義資料顯示”中瞭解有關繫結和資料模板的更多資訊。

如果不支援WPF中的命令,則MVVM模式的功能將大大降低。在本文中,我將向您展示ViewModel如何將命令公開給View,從而允許檢視使用其功能。如果您對命令不熟悉,建議您閱讀2008年9月版的Brian Noyes的綜合文章“高階WPF:瞭解WPF中的路由事件和命令”。

除了WPF(和Silverlight 2)功能使MVVM成為構建應用程式的自然方式之外,該模式也很流行,因為ViewModel類易於進行單元測試。當應用程式的互動邏輯位於一組ViewModel類中時,您可以輕鬆編寫測試它的程式碼。從某種意義上說,檢視和單元測試只是兩種不同型別的ViewModel使用者。為應用程式的ViewModel擁有一套測試可以提供免費和快速的迴歸測試,這有助於降低隨著時間的推移維護應用程式的成本。

除了促進建立自動迴歸測試之外,ViewModel類的可測試性還可以幫助正確設計易於使用的使用者介面。在設計應用程式時,通常可以通過想象要編寫一個使用ViewModel的單元測試來決定是在檢視中還是在ViewModel中。如果可以在不建立任何UI物件的情況下為ViewModel編寫單元測試,則還可以完全使ViewModel外觀化,因為它不依賴於特定的可視元素。

最後,對於與視覺設計師合作的開發人員,使用MVVM可以更輕鬆地建立流暢的設計師/開發人員工作流程。由於檢視只是ViewModel的任意使用者,因此很容易將一個檢視撕下並放入新檢視以呈現ViewModel。這個簡單的步驟可以使設計人員快速建立原型並評估使用者介面。

開發團隊可以專注於建立健壯的ViewModel類,而設計團隊可以專注於建立使用者友好的View。連線兩個團隊的輸出僅涉及確保檢視的XAML檔案中存在正確的繫結而已。

演示應用

在這一點上,我已經回顧了MVVM的歷史和操作理論。我還研究了為什麼它在WPF開發人員中如此受歡迎。現在是時候收起袖子,看看模式在起作用。本文隨附的演示應用程式以多種方式使用MVVM。它提供了豐富的示例資源,以幫助將概念引入有意義的上下文中。我在Visual Studio 2008 SP1中針對Microsoft .NET Framework 3.5 SP1建立了演示應用程式。單元測試在Visual Studio單元測試系統中執行。

該應用程式可以包含任意數量的“工作區”,使用者可以通過單擊左側導航區域中的命令連結來開啟每個工作區。所有工作空間都位於主內容區域上的TabControl中。使用者可以通過單擊該工作區的選項卡項上的“關閉”按鈕來關閉該工作區。該應用程式有兩個可用的工作區:“所有客戶”和“新客戶”。執行該應用程式並開啟一些工作區後,UI類似於圖1。

圖1工作區

一次只能開啟“所有客戶”工作區的一個例項,但是一次可以開啟任何數量的“新客戶”工作區。當用戶決定建立新客戶時,她必須填寫圖2中的資料輸入表單。

圖2新客戶資料輸入表單

在使用有效值填寫資料輸入表單並單擊“儲存”按鈕之後,新客戶的名稱將顯示在選項卡項中,並且該客戶將新增到所有客戶的列表中。該應用程式不支援刪除或編輯現有客戶,但是通過在現有應用程式體系結構上進行構建,可以輕鬆實現該功能以及與之類似的許多其他功能。現在,您已經對演示應用程式的功能有了一個高階的瞭解,讓我們研究一下它是如何設計和實現的。

中繼命令邏輯

應用程式中的每個檢視都有一個空的程式碼隱藏檔案,但在類的建構函式中呼叫InitializeComponent的標準樣板程式碼除外。實際上,您可以從專案中刪除檢視的程式碼隱藏檔案,並且該應用程式仍將正確編譯並執行。儘管檢視中沒有事件處理方法,但是當用戶單擊按鈕時,應用程式會做出反應並滿足使用者的請求。這之所以有效,是因為在UI中顯示的Hyperlink,Button和MenuItem控制元件的Command屬性上建立了繫結。這些繫結確保當用戶單擊控制元件時,將執行ViewModel公開的ICommand物件。您可以將命令物件視為一個介面卡,可以輕鬆地從XAML中宣告的檢視中使用ViewModel的功能。

當ViewModel公開ICommand型別的例項屬性時,命令物件通常使用該ViewModel物件來完成其工作。一種可能的實現模式是在ViewModel類內建立一個私有巢狀類,以便該命令可以訪問其包含ViewModel的私有成員,並且不會汙染名稱空間。該巢狀類實現ICommand介面,並將對包含的ViewModel物件的引用注入其建構函式中。但是,為ViewModel公開的每個命令建立實現ICommand的巢狀類,可能會使ViewModel類的大小膨脹。更多的程式碼意味著更大的潛在錯誤。

在演示應用程式中,RelayCommand類解決了此問題。RelayCommand允許您通過傳遞給其建構函式的委託來注入命令的邏輯。這種方法允許在ViewModel類中簡潔,簡潔地執行命令。RelayCommand是Microsoft Composite Application Library中的DelegateCommand的簡化變體。RelayCommand類如圖3所示。

圖3 RelayCommand類

public class RelayCommand : ICommand
{
    #region Fields 
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;
    #endregion // Fields 
    #region Constructors 
    public RelayCommand(Action<object> execute) : this(execute, null) { }
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        _execute = execute; _canExecute = canExecute;
    }
    #endregion // Constructors 
    #region ICommand Members 
    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
    public void Execute(object parameter) { _execute(parameter); }
    #endregion // ICommand Members 
}

作為ICommandinterface實現的一部分的CanExecuteChanged事件具有一些有趣的功能。它將事件訂閱委派給CommandManager.RequerySuggested事件。這樣可以確保WPF命令基礎結構在詢問內建命令時詢問所有RelayCommand物件是否可以執行。以下來自CustomerViewModel類的程式碼(我將在以後進行深入研究)顯示瞭如何使用lambda表示式配置RelayCommand:

RelayCommand _saveCommand; 
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null) {
            _saveCommand = new RelayCommand(param => this.Save(), 
                param => this.CanSave);
        }
        return _saveCommand;
    }
}
ViewModel類層次結構

大多數ViewModel類需要相同的功能。他們通常需要實現INotifyPropertyChanged介面,通常需要具有使用者友好的顯示名稱,對於工作空間,他們需要具有關閉功能(即,從UI中刪除)。這個問題自然會導致建立一個或兩個ViewModel基類,以便新的ViewModel類可以繼承基類的所有常用功能。ViewModel類構成圖4中所示的繼承層次結構。


圖4繼承層次結構

絕不是所有ViewModel的基類。如果您希望通過將許多較小的類組合在一起而不是使用繼承來獲得類中的功能,那麼這不是問題。就像任何其他設計模式一樣,MVVM是一組準則,而不是規則。