喬什·史密斯| 2009年2月 以前有一些類似的設(shè)計可以幫助馴服使用這種動物的模式,但是粗略地解決和解決問題的方式可能很困難,。所有努力,。 它永遠不會設(shè)計模式。有時我們使用復(fù)雜的設(shè)計模式,,這需要編寫更大量的代碼,,因為使用的 UI 平臺不適合簡單的模式。它們需要一個平臺,,可以使用簡單,、經(jīng)過時間安排、開發(fā)人員認可的設(shè)計模式簡化了 UI,。就是,,Windows Presentation Foundation (WPF) 提供了一個點。 軟件世界越來越多地采用 WPF,,WPF 社區(qū)一直在開發(fā)自己的模式和實踐生態(tài)系統(tǒng),。在本文中,我將回顧一些使用 WPF 設(shè)計和實現(xiàn)客戶端應(yīng)用程序的最佳實踐,。通過結(jié)合模型-view-view (MVVM) 設(shè)計模型利用 WPF 的一些簡單核心功能,,我將通過一個示例程序演示以“正確”構(gòu)建 WPF 應(yīng)用程序的方式是多么簡單。 在此結(jié)束時,,將有明確的數(shù)據(jù)模板,、命令、數(shù)據(jù)綁定,、資源系統(tǒng)和 MVVM 模式如何組合創(chuàng)建一個簡單,、可測試、健壯的框架,任何 WPF 應(yīng)用程序都可以在該框架上茁壯成長,。應(yīng)用程序的演示程序可以作為 MVVM 程序其核心架構(gòu)的真實 W 應(yīng)用程序的模板,。演示解決方案中的單元測試使用作為當應(yīng)用程序的顯示功能存在于示例視圖模型中時,測試功能是這樣的容易,。在深入了解像細節(jié),,讓我們先回顧一下之前之前為什么要使用MVVM這樣的模式,。 秩序與秩序在簡單的“Hello, World!”中使用設(shè)計模式是不必要的,,而且會適得其反。程序,。任何一個稱職的開發(fā)者都能一眼看懂幾行代碼,。但是,隨著程序中功能數(shù)量的增加,,代碼行數(shù)和移動部件的數(shù)量也會相應(yīng)增加,。最終,系統(tǒng)的復(fù)雜性和它所包含的反復(fù)出現(xiàn)的問題會鼓勵開發(fā)人員以這樣一種方式組織他們的代碼:更容易理解,、討論,、擴展和排除故障。我們通過將眾所周知的名稱應(yīng)用于源代碼中的某些實體來減少復(fù)雜系統(tǒng)的認知混亂,。我們通過考慮其在系統(tǒng)中的功能角色來確定應(yīng)用于一段代碼的名稱,。 開發(fā)人員經(jīng)常有意地根據(jù)設(shè)計模式構(gòu)建他們的代碼,而不是讓模式有機地出現(xiàn),。這兩種方法都沒有錯,,但在本文中,我將研究明確使用 MVVM 作為 WPF 應(yīng)用程序架構(gòu)的好處,。某些類的名稱包括來自 MVVM 模式的眾所周知的術(shù)語,,例如,如果類是視圖的抽象,,則以“ViewModel”結(jié)尾,。這種方法有助于避免前面提到的認知混亂。相反,,您可以愉快地生活在一種受控的混亂狀態(tài)中,,這是大多數(shù)專業(yè)軟件開發(fā)項目中的自然狀態(tài)! Model-View-ViewModel 的演進自從人們開始創(chuàng)建軟件用戶界面以來,,就出現(xiàn)了流行的設(shè)計模式來幫助它變得更容易,。例如,Model-View-Presenter (MVP) 模式在各種 UI 編程平臺上很受歡迎,。MVP 是 Model-View-Controller 模式的一種變體,,已經(jīng)存在了幾十年。如果您以前從未使用過 MVP 模式,這里有一個簡化的解釋,。您在屏幕上看到的是 View,,它顯示的數(shù)據(jù)是模型,Presenter 將兩者掛鉤,。視圖依賴于 Presenter 來填充模型數(shù)據(jù),、對用戶輸入做出反應(yīng)、提供輸入驗證(可能通過委托給模型)以及其他此類任務(wù),。如果您想了解有關(guān)模型視圖演示器的更多信息,,我建議您閱讀 Jean-Paul Boodhoo 的2006 年 8 月設(shè)計模式專欄。 早在 2004 年,,Martin Fowler 發(fā)表了一篇關(guān)于名為Presentation Model (PM) 的模式的文章,。PM 模式類似于 MVP,因為它將視圖與其行為和狀態(tài)分開,。PM 模式的有趣部分是創(chuàng)建了一個視圖的抽象,,稱為表示模型。那么,,一個視圖就變成了一個表示模型的呈現(xiàn),。在 Fowler 的解釋中,他表明 Presentation Model 經(jīng)常更新其 View,,從而使兩者保持同步,。該同步邏輯作為代碼存在于 Presentation Model 類中。 2005 年,,現(xiàn)任微軟 WPF 和 Silverlight 架構(gòu)師之一的 John Gossman在他的博客上公開了Model-View-ViewModel (MVVM) 模式,。MVVM 與 Fowler 的 Presentation Model 相同,因為這兩種模式都具有對 View 的抽象,,其中包含 View 的狀態(tài)和行為,。Fowler 引入了 Presentation Model 作為一種創(chuàng)建獨立于 UI 平臺的 View 抽象的方法,而 Gossman 引入了 MVVM 作為一種利用 WPF 核心特性來簡化用戶界面創(chuàng)建的標準化方法,。從這個意義上說,,我認為 MVVM 是更通用的 PM 模式的專門化,專為 WPF 和 Silverlight 平臺量身定制,。 在 Glenn Block 2008 年 9 月號的優(yōu)秀文章“ Prism: Patterns for Building Composite Applications with WPF ”中,,他解釋了 Microsoft Composite Application Guidance for WPF。從未使用過 ViewModel 一詞,。相反,,術(shù)語表示模型用于描述視圖的抽象。然而,,在整篇文章中,,我將把該模式稱為 MVVM,,將視圖的抽象稱為 ViewModel。我發(fā)現(xiàn)這個術(shù)語在 WPF 和 Silverlight 社區(qū)中更為流行,。 與 MVP 中的 Presenter 不同,,ViewModel 不需要對視圖的引用。視圖綁定到 ViewModel 上的屬性,,這反過來又會公開模型對象中包含的數(shù)據(jù)以及視圖特定的其他狀態(tài),。視圖和 ViewModel 之間的綁定很容易構(gòu)造,因為 ViewModel 對象被設(shè)置為視圖的 DataContext,。如果 ViewModel 中的屬性值發(fā)生變化,,這些新值會通過數(shù)據(jù)綁定自動傳播到視圖。當用戶單擊視圖中的按鈕時,,視圖模型上的命令將執(zhí)行以執(zhí)行請求的操作,。ViewModel,而不是 View,,執(zhí)行對模型數(shù)據(jù)所做的所有修改。 視圖類不知道模型類的存在,,而 ViewModel 和模型不知道視圖,。事實上,模型完全沒有注意到 ViewModel 和視圖存在的事實,。釷is 是一個非常松散耦合的設(shè)計,,它在很多方面都帶來了好處,你很快就會看到,。 為什么 WPF 開發(fā)人員喜歡 MVVM一旦開發(fā)人員熟悉 WPF 和 MVVM,,就很難區(qū)分這兩者。MVVM 是 WPF 開發(fā)人員的通用語言,,因為它非常適合 WPF 平臺,,并且 WPF 旨在使使用 MVVM 模式(以及其他模式)構(gòu)建應(yīng)用程序變得容易。事實上,,微軟在內(nèi)部使用 MVVM 開發(fā) WPF 應(yīng)用程序,,例如 Microsoft Expression Blend,而核心 WPF 平臺正在建設(shè)中,。WPF 的許多方面,,例如無外觀控制模型和數(shù)據(jù)模板,都利用了 MVVM 促進的顯示與狀態(tài)和行為的強分離,。 WPF 的一個最重要的方面使 MVVM 成為一個很好的使用模式是數(shù)據(jù)綁定基礎(chǔ)結(jié)構(gòu),。通過將視圖的屬性綁定到 ViewModel,您可以在兩者之間獲得松散耦合,,并且完全無需在直接更新視圖的 ViewModel 中編寫代碼,。數(shù)據(jù)綁定系統(tǒng)還支持輸入驗證,它提供了一種將驗證錯誤傳輸?shù)揭晥D的標準化方式。 WPF 的另外兩個使這種模式如此可用的特性是數(shù)據(jù)模板和資源系統(tǒng),。數(shù)據(jù)模板將視圖應(yīng)用于用戶界面中顯示的 ViewModel 對象,。您可以在 XAML 中聲明模板,并讓資源系統(tǒng)在運行時自動為您定位和應(yīng)用這些模板,。您可以在我 2008 年 7 月的文章“數(shù)據(jù)和 WPF:使用數(shù)據(jù)綁定和 WPF 自定義數(shù)據(jù)顯示”中了解有關(guān)綁定和數(shù)據(jù)模板的更多信息,。 如果不是 WPF 中對命令的支持,MVVM 模式的功能就會少很多,。在本文中,,我將向您展示 ViewModel 如何向 View 公開命令,從而允許 View 使用其功能,。如果您不熟悉命令,,我建議您閱讀2008 年 9 月號的 Brian Noyes 的綜合文章“高級 WPF:了解 WPF 中的路由事件和命令”。 除了使 MVVM 成為構(gòu)建應(yīng)用程序的自然方式的 WPF(和 Silverlight 2)特性之外,,該模式也很受歡迎,,因為 ViewModel 類易于進行單元測試。當應(yīng)用程序的交互邏輯存在于一組 ViewModel 類中時,,您可以輕松編寫測試它的代碼,。從某種意義上說,視圖和單元測試只是兩種不同類型的 ViewModel 消費者,。為應(yīng)用程序的 ViewModel 提供一套測試可提供免費且快速的回歸測試,,這有助于降低長期維護應(yīng)用程序的成本。 除了促進自動化回歸測試的創(chuàng)建之外,,ViewModel 類的可測試性還有助于正確設(shè)計易于換膚的用戶界面,。在設(shè)計應(yīng)用程序時,您通??梢酝ㄟ^想象您想要編寫一個單元測試來使用 ViewModel 來決定是應(yīng)該在視圖中還是在 ViewModel 中,。如果您可以在不創(chuàng)建任何 UI 對象的情況下為 ViewModel 編寫單元測試,那么您也可以完全為 ViewModel 蒙皮,,因為它不依賴于特定的可視元素,。 最后,對于與視覺設(shè)計師合作的開發(fā)人員,,使用 MVVM 可以更輕松地創(chuàng)建流暢的設(shè)計師/開發(fā)人員工作流程,。由于視圖只是 ViewModel 的任意消費者,它很容易只撕掉一個視圖并放入一個新視圖來渲染一個 ViewModel,。這個簡單的步驟允許對設(shè)計師制作的用戶界面進行快速原型設(shè)計和評估,。 開發(fā)團隊可以專注于創(chuàng)建健壯的 ViewModel 類,而設(shè)計團隊可以專注于制作用戶友好的視圖,。連接兩個團隊的輸出只需確保視圖的 XAML 文件中存在正確的綁定,。 演示應(yīng)用程序至此,,我回顧了 MVVM 的歷史和運行原理。我還研究了為什么它在 WPF 開發(fā)人員中如此受歡迎,。現(xiàn)在是時候卷起袖子,,看看實際的模式了。本文附帶的演示應(yīng)用程序以多種方式使用 MVVM,。它提供了豐富的示例資源,,有助于將概念置于有意義的上下文中。我在 Visual Studio 2008 SP1 中針對 Microsoft .NET Framework 3.5 SP1 創(chuàng)建了演示應(yīng)用程序,。單元測試在 Visual Studio 單元測試系統(tǒng)中運行,。 該應(yīng)用程序可以包含任意數(shù)量的“工作區(qū)”,用戶可以通過單擊左側(cè)導(dǎo)航區(qū)域中的命令鏈接來打開每個“工作區(qū)”,。所有工作區(qū)都位于主要內(nèi)容區(qū)域的 TabControl 中,。用戶可以通過單擊工作區(qū)選項卡項上的關(guān)閉按鈕來關(guān)閉工作區(qū)。該應(yīng)用程序有兩個可用的工作區(qū):“所有客戶”和“新客戶”,。運行應(yīng)用程序并打開一些工作區(qū)后,,UI 類似于圖 1。
一次只能打開“所有客戶”工作區(qū)的一個實例,,但一次可以打開任意數(shù)量的“新客戶”工作區(qū),。當用戶決定創(chuàng)建一個新客戶時,她必須填寫圖 2中的數(shù)據(jù)輸入表單,。
使用有效值填寫數(shù)據(jù)輸入表單并單擊“保存”按鈕后,新客戶的名稱將出現(xiàn)在選項卡項中,,并且該客戶將添加到所有客戶的列表中,。該應(yīng)用程序不支持刪除或編輯現(xiàn)有客戶,但通過在現(xiàn)有應(yīng)用程序架構(gòu)之上構(gòu)建,,該功能以及許多其他類似功能很容易實現(xiàn),。現(xiàn)在您已經(jīng)對演示應(yīng)用程序的功能有了更高層次的了解,讓我們研究一下它是如何設(shè)計和實現(xiàn)的,。 中繼命令邏輯應(yīng)用程序中的每個視圖都有一個空的代碼隱藏文件,,除了在類的構(gòu)造函數(shù)中調(diào)用 InitializeComponent 的標準樣板代碼。事實上,,您可以從項目中刪除視圖的代碼隱藏文件,,并且應(yīng)用程序仍然可以正確編譯和運行。盡管視圖中缺少事件處理方法,,但當用戶單擊按鈕時,,應(yīng)用程序會做出反應(yīng)并滿足用戶的請求。這是因為在 UI 中顯示的 Hyperlink,、Button 和 MenuItem 控件的 Command 屬性上建立了綁定,。這些綁定確保當用戶單擊控件時,,由 ViewModel 公開的 ICommand 對象執(zhí)行。您可以將命令對象視為一個適配器,,它可以輕松地從 XAML 中聲明的視圖中使用 ViewModel 的功能,。 當 ViewModel 公開 ICommand 類型的實例屬性時,命令對象通常使用該 ViewModel 對象來完成其工作,。一種可能的實現(xiàn)模式是在 ViewModel 類中創(chuàng)建一個私有嵌套類,,以便命令可以訪問其包含 ViewModel 的私有成員并且不會污染命名空間。該嵌套類實現(xiàn)了 ICommand 接口,,并將對包含 ViewModel 對象的引用注入到其構(gòu)造函數(shù)中,。但是,為 ViewModel 公開的每個命令創(chuàng)建一個實現(xiàn) ICommand 的嵌套類可能會使 ViewModel 類的大小膨脹,。更多的代碼意味著更大的潛在錯誤,。 在演示應(yīng)用程序中,RelayCommand 類解決了這個問題,。RelayCommand 允許您通過傳遞給其構(gòu)造函數(shù)的委托來注入命令的邏輯,。這種方法允許簡潔、簡潔ViewModel 類中的ise命令實現(xiàn),。RelayCommand 是Microsoft 復(fù)合應(yīng)用程序庫中的 DelegateCommand 的簡化變體,。RelayCommand 類如圖 3所示。 圖 3 RelayCommand 類 C#
CanExecuteChanged 事件是 ICommandinterface 實現(xiàn)的一部分,,它有一些有趣的特性,。它將事件訂閱委托給 CommandManager.RequerySuggested 事件。這確保 WPF 命令基礎(chǔ)結(jié)構(gòu)在詢問內(nèi)置命令時詢問所有 RelayCommand 對象是否可以執(zhí)行,。以下來自 CustomerViewModel 類的代碼(稍后我將深入研究)展示了如何使用 lambda 表達式配置 RelayCommand: C#
ViewModel 類層次結(jié)構(gòu)大多數(shù) ViewModel 類都需要相同的功能,。它們通常需要實現(xiàn) INotifyPropertyChanged 接口,它們通常需要有一個用戶友好的顯示名稱,,并且,,對于工作區(qū),它們需要能夠關(guān)閉(即從 UI 中移除),。這個問題自然會導(dǎo)致創(chuàng)建一個或兩個 ViewModel 基類,,以便新的 ViewModel 類可以繼承基類的所有通用功能。ViewModel 類形成了圖 4中所示的繼承層次結(jié)構(gòu),。
Having a base class for all of your ViewModels is by no means a requirement. If you prefer to gain features in your classes by composing many smaller classes together, instead of using inheritance, that is not a problem. Just like any other design pattern, MVVM is a set of guidelines, not rules. ViewModelBase ClassViewModelBase is the root class in the hierarchy, which is why it implements the commonly used INotifyPropertyChanged interface and has a DisplayName property. The INotifyPropertyChanged interface contains an event called PropertyChanged. Whenever a property on a ViewModel object has a new value, it can raise the PropertyChanged event to notify the WPF binding system of the new value. Upon receiving that notification, the binding system queries the property, and the bound property on some UI element receives the new value. In order for WPF to know which property on the ViewModel objecthas changed, the PropertyChangedEventArgs class exposes a PropertyName property of type String. You must be careful to pass the correct property name into that event argument; otherwise, WPF will end up querying the wrong property for a new value. One interesting aspect of ViewModelBase is that it provides the ability to verify that a property with a given name actually exists on the ViewModel object. Th這在重構(gòu)時非常有用,,因為通過 Visual Studio 2008 重構(gòu)功能更改屬性名稱不會更新源代碼中恰好包含該屬性名稱的字符串(也不應(yīng)該)。在事件參數(shù)中使用不正確的屬性名稱引發(fā) PropertyChanged 事件可能會導(dǎo)致難以追蹤的細微錯誤,,因此這個小功能可以節(jié)省大量時間,。圖 5顯示了來自 ViewModelBase 的添加這種有用支持的代碼。 圖 5 驗證屬性 C#
CommandViewModel 類最簡單的具體 ViewModelBase 子類是 CommandViewModel,。它公開了一個名為 Command 的 ICommand 類型的屬性,。MainWindowViewModel 通過其 Commands 屬性公開這些對象的集合,。主窗口左側(cè)的導(dǎo)航區(qū)域顯示 MainWindowViewModel 公開的每個 CommandViewModel 的鏈接,例如“查看所有客戶”和“創(chuàng)建新客戶”,。當用戶單擊鏈接并執(zhí)行其中一個命令時,,將在主窗口的 TabControl 中打開一個工作區(qū)。CommandViewModel 類定義如下所示: C#
在 MainWindowResources.xaml 文件中存在一個 DataTemplate,,其鍵為“CommandsTemplate”,。MainWindow 使用該模板來呈現(xiàn)前面提到的 CommandViewModel 集合。該模板只是將每個 CommandViewModel 對象呈現(xiàn)為 ItemsControl 中的鏈接,。每個超鏈接的 Command 屬性都綁定到 CommandViewModel 的 Command 屬性,。該 XAML如圖 6所示。 圖 6 渲染命令列表 XML
MainWindowViewModel 類正如前面在類圖中看到的,,WorkspaceViewModel 類派生自 ViewModelBase 并添加了關(guān)閉功能,。通過“關(guān)閉”,我的意思是在運行時從用戶界面中刪除了工作區(qū),。三個類派生自 WorkspaceViewModel:MainWindowViewModel,、AllCustomersViewModel 和 CustomerViewModel。MainWindowViewModel 的關(guān)閉請求由 App 類處理,,該類創(chuàng)建 MainWindow 及其 ViewModel,,如圖 7 所示。 圖 7 創(chuàng)建 ViewModel C#
MainWindow 包含一個菜單項,,其 Command 屬性綁定到 MainWindowViewModel 的 CloseCommand 屬性,。當用戶單擊該菜單項時,App 類通過調(diào)用窗口的 Close 方法進行響應(yīng),,如下所示: XML
MainWindowViewModel 包含一個可觀察的 WorkspaceViewModel 對象集合,,稱為 Workspaces。主窗口包含一個 TabControl,,其 ItemsSource 屬性綁定到該集合。每個選項卡項都有一個關(guān)閉按鈕,,其 Command 屬性綁定到其對應(yīng)的 WorkspaceViewModel 實例的 CloseCommand,。配置每個選項卡項的模板的精簡版本顯示在下面的代碼中。該代碼位于 MainWindowResources.xaml 中,,該模板解釋了如何使用關(guān)閉按鈕呈現(xiàn)選項卡項: XML
當用戶單擊選項卡項中的關(guān)閉按鈕時,,WorkspaceViewModel 的 CloseCommand 會執(zhí)行,從而觸發(fā)其 RequestClose 事件,。MainWindowViewModel 監(jiān)視其工作區(qū)的 RequestClose 事件,,并根據(jù)請求從 Workspaces 集合中刪除工作區(qū)。由于 MainWindow 的 TabControl 將其 ItemsSource 屬性綁定到 WorkspaceViewModels 的 observable 集合,,因此從集合中刪除項目會導(dǎo)致相應(yīng)的工作區(qū)從 TabControl 中刪除,。MainWindowViewModel 中的邏輯如圖 8所示,。 圖 8 從 UI 中刪除工作區(qū) C#
在 UnitTests 項目中,MainWindowViewModelTests.cs 文件包含一個測試方法,,用于驗證此功能是否正常工作,。可以輕松地為 ViewModel 類創(chuàng)建單元測試是 MVVM 模式的一個巨大賣點,因為它允許對應(yīng)用程序功能進行簡單測試,,而無需編寫涉及 UI 的代碼,。該測試方法如圖 9所示。 圖 9 測試方法 C#
將視圖應(yīng)用于 ViewModelMainWindowViewModel 間接向主窗口的 TabControl 添加和刪除 WorkspaceViewModel 對象,。通過依賴數(shù)據(jù)綁定,,aTabItem 的 Content 屬性接收到一個 ViewModelBase 派生的對象來顯示。ViewModelBase 不是 UI 元素,,因此它本身沒有對渲染的內(nèi)在支持,。默認情況下,在 WPF 中,,通過在 TextBlock 中顯示對其 ToString 方法的調(diào)用結(jié)果來呈現(xiàn)非可視對象,。這顯然不是您所需要的,除非您的用戶迫切希望看到我們的 ViewModel 類的類型名稱,! 您可以使用類型化的 DataTemplate 輕松告訴 WPF 如何呈現(xiàn) ViewModel 對象,。類型化的 DataTemplate 沒有分配給它的 anx:Key 值,但它確實將其 DataType 屬性設(shè)置為 Type 類的實例,。如果 WPF 嘗試呈現(xiàn)您的 ViewModel 對象之一,,它將檢查資源系統(tǒng)是否在范圍內(nèi)具有類型化的 DataTemplate,其 DataType 與您的 ViewModel 對象的類型相同(或基類),。如果找到,,它將使用該模板來呈現(xiàn)選項卡項的 Content 屬性引用的 ViewModel 對象。 MainWindowResources.xaml 文件有一個 ResourceDictionary,。該字典被添加到主窗口的資源層次結(jié)構(gòu)中,,這意味著它包含的資源在窗口的資源范圍內(nèi)。當標簽項的內(nèi)容設(shè)置為ViewModelObject時,,來自此字典的鍵入的數(shù)據(jù)模式為呈現(xiàn)它的視圖(即用戶控件),,如圖10所示。 圖 10 提供視圖 XML
您無需編寫任何代碼來確定要為 ViewModel 對象顯示哪個視圖,。WPF 資源系統(tǒng)為您完成所有繁重的工作,,讓您可以專注于更重要的事情。在更復(fù)雜的場景中,,可以以編程方式選擇視圖,,但在大多數(shù)情況下這是不必要的。 數(shù)據(jù)模型和存儲庫您已經(jīng)了解了 ViewModel 對象是如何被應(yīng)用程序外殼加載,、顯示和關(guān)閉的,。現(xiàn)在通用管道已經(jīng)到位,,您可以查看更具體到應(yīng)用程序域的實現(xiàn)細節(jié)。在深入了解應(yīng)用程序的兩個工作區(qū)“所有客戶”和“新客戶”之前,,讓我們首先檢查數(shù)據(jù)模型和數(shù)據(jù)訪問類,。這些類的設(shè)計幾乎與 MVVM 模式無關(guān),因為您可以創(chuàng)建一個 ViewModel 類來將幾乎任何數(shù)據(jù)對象調(diào)整為對 WPF 友好的東西,。 演示程序中唯一的模型類是 Customer,。該類具有一些屬性,這些屬性表示有關(guān)公司客戶的信息,,例如他們的名字,、姓氏和電子郵件地址。它通過實現(xiàn)標準 IDataErrorInfo 接口來提供驗證消息,,該接口在 WPF 出現(xiàn)之前已經(jīng)存在多年,。Customer 類中沒有任何內(nèi)容表明它正在用于 MVVM 架構(gòu)甚至 WPF 應(yīng)用程序中。該類很容易來自遺留業(yè)務(wù)庫,。 數(shù)據(jù)必須來自并存在于某個地方,。在此應(yīng)用程序中,CustomerRepository 類的實例加載并存儲所有Customer 對象,。它恰好從 XML 文件加載客戶數(shù)據(jù),,但外部數(shù)據(jù)源的類型無關(guān)緊要。數(shù)據(jù)可能來自數(shù)據(jù)庫,、Web 服務(wù),、命名管道、磁盤上的文件,,甚至是信鴿:這無關(guān)緊要,。只要您有一個包含一些數(shù)據(jù)的 .NET 對象,無論它來自何處,,MVVM 模式都可以在屏幕上獲取該數(shù)據(jù),。 CustomerRepository 類公開了一些方法,這些方法允許您獲取所有可用的 Customer 對象,,將新的 Customer 添加到存儲庫,,并檢查 Customer 是否已經(jīng)在存儲庫中。由于應(yīng)用程序不允許用戶刪除客戶,,存儲庫不允許您刪除客戶。當新客戶通過 AddCustomer 方法進入 CustomerRepository 時會觸發(fā) CustomerAdded 事件,。 顯然,,與實際業(yè)務(wù)應(yīng)用程序所需的數(shù)據(jù)相比,該應(yīng)用程序的數(shù)據(jù)模型非常小,,但這并不重要,。了解什么很重要是ViewModel 類如何使用 Customer 和 CustomerRepository,。請注意,CustomerViewModel 是 Customer 對象的包裝器,。它通過一組屬性公開客戶的狀態(tài),,以及客戶視圖控件使用的其他狀態(tài)。CustomerViewModel 不復(fù)制客戶的狀態(tài),;它只是通過委托將其公開,,如下所示: C#
當用戶創(chuàng)建新客戶并單擊 CustomerView 控件中的 Save 按鈕時,與該視圖關(guān)聯(lián)的 CustomerViewModel 會將新的 Customer 對象添加到 CustomerRepository,。這會導(dǎo)致存儲庫的 CustomerAdded 事件觸發(fā),,這讓 AllCustomersViewModel 知道它應(yīng)該將新的 CustomerViewModel 添加到其 AllCustomers 集合中。從某種意義上說,,CustomerRepository 充當了處理客戶對象的各種 ViewModel 之間的異步機制,。也許有人會認為這是使用中介者設(shè)計模式。我將在接下來的部分中詳細了解其工作原理,,但現(xiàn)在請參閱圖 11中的圖表,,以從高級別的角度了解所有部分如何組合在一起。 圖 11 客戶關(guān)系 新客戶數(shù)據(jù)輸入表當用戶單擊“創(chuàng)建新客戶”鏈接時,,MainWindowViewModel 將新的 CustomerViewModel 添加到其工作區(qū)列表中,,并且 CustomerView 控件顯示它。用戶在輸入字段中輸入有效值后,,保存按鈕進入啟用狀態(tài),,以便用戶可以持久保存新的客戶信息。這里沒有什么特別之處,,只是一個帶有輸入驗證和保存按鈕的常規(guī)數(shù)據(jù)輸入表單,。 The Customer class has built-in validation support, available through its IDataErrorInfo interface implementation. That validation ensures the customer has a first name, a well-formed e-mail address, and, if the customer is a person, a last name. If the Customer's IsCompany property returns true, the LastName property cannot have a value (the idea being that a company does not have a last name). This validation logic might make sense from the Customer object's perspective, but it does not meet the needs of the user interface. The UI requires a user to select whether a new customer is a person or a company. The Customer Type selector initially has the value "(Not Specified)". How can the UI tell the user that the customer type is unspecified if the IsCompanyproperty of a Customer only allows for a true or false value? Assuming you have complete control over the entire software system, you could change the IsCompany property to be of typeNullable, which would allow for the "unselected" value. However,the real world is not always so simple. Suppose you cannot changethe Customer class because it comes from a legacy library owned bya different team in your company. What if there is no easy way to persist that "unselected" value because of the existing database schema? What if other applications already use the Customer classand rely on the property being a normal Boolean value? Once again,having a ViewModel comes to the rescue. The test method in Figure 12 shows how this functionality works in CustomerViewModel. CustomerViewModel exposes a CustomerTypeOptions property so that the Customer Type selector has three strings to display. It also exposes a CustomerTypeproperty, which stores the selected String in the selector. When CustomerType is set, it maps the String value to a Boolean value for the underlying Customer object's IsCompanyproperty. Figure 13 shows the two properties. Figure 12 The Test Method C#
Figure 13 CustomerType Properties C#
The CustomerView control contains a ComboBox that is bound to those properties, as seen here: XML
When the selected item in that ComboBox changes, the datasource's IDataErrorInfo interface is queried to see if the new value is valid. That occurs because the SelectedItem property binding has ValidatesOnDataErrors set to true. Since the data source is a CustomerViewModel object, the binding system asks that CustomerViewModel for a validation error on the CustomerTypeproperty. Most of the time, CustomerViewModel delegates all requests for validation errors to the Customer object it contains.However, since Customer has no notion of having an unselected state for the IsCompany property, the CustomerViewModel class must handle validating the new selected item in the ComboBox control. That code is seen in Figure 14. Figure 14 Validating a CustomerViewModel Object C#
The key aspect of this code is that CustomerViewModel's implementation of IDataErrorInfo can handle requests for ViewModel-specific property validation and delegate the other requests to the Customer object. This allows you to make use of validation logic in Model classes and have additional validation for properties that only make sense to ViewModel classes. The ability to save a CustomerViewModel is available to a view through the SaveCommand property. That command uses the RelayCommand class examined earlier to allow CustomerViewModel to decide if it can save itself and what to do when told to save its state. In this application, saving a new customer simply means adding it to a CustomerRepository. Deciding if the new customer isready to be saved requires consent from two parties. The Customer object must be asked if it is valid or not, and theCustomerViewModel must decide if it is valid. This two-part decision is necessary because of the ViewModel-specific properties and validation examined previously. The save logic forCustomerViewModel is shown in Figure 15. Figure 15 The Save Logic for CustomerViewModel C#
在這里使用 ViewModel 可以更輕松地創(chuàng)建可以顯示 Customer 對象并允許布爾屬性的“未選擇”狀態(tài)之類的事情。它還提供了輕松告訴客戶保存其狀態(tài)的能力,。如果視圖直接綁定到 Customer 對象,,則視圖將需要大量代碼才能使其正常工作。在設(shè)計良好的 MVVM 架構(gòu)中,,大多數(shù)視圖背后的代碼應(yīng)該是空的,,或者最多只包含操作該視圖中包含的控件和資源的代碼。有時還需要在 View 的代碼隱藏中編寫與 ViewModel 對象交互的代碼,,例如掛鉤事件或調(diào)用將很難從 ViewModel 本身調(diào)用的方法,。 所有客戶視圖演示應(yīng)用程序還包含一個在 ListView 中顯示所有客戶的工作區(qū)。列表中的客戶根據(jù)他們是公司還是個人進行分組,。用戶可以一次選擇一個或多個客戶,,并在右下角查看他們的總銷售額。 UI 是 AllCustomersView 控件,它呈現(xiàn)一個AllCustomersViewModel 對象,。每個 ListViewItem 代表由 AllCustomerViewModel 對象公開的 AllCustomers 集合中的一個 CustomerViewModel 對象,。在上一節(jié)中,您看到了 CustomerViewModel 如何呈現(xiàn)為數(shù)據(jù)輸入表單,,現(xiàn)在完全相同的 CustomerViewModel 對象被呈現(xiàn)為 ListView 中的一個項目,。CustomerViewModel 類不知道顯示它的視覺元素是什么,這就是為什么這種重用是可能的,。 AllCustomersView 創(chuàng)建在 ListView 中看到的組,。它通過將 ListView 的 ItemsSource 綁定到一個 CollectionViewSource 來實現(xiàn)這一點,如圖16所示,。 圖 16 CollectionViewSource XML
ListViewItem 和 CustomerViewModel 對象之間的關(guān)聯(lián)是由 ListView 的 ItemContainerStyle 屬性建立的,。分配給該屬性的樣式將應(yīng)用于每個 ListViewItem,這使 ListViewItem 上的屬性能夠綁定到 CustomerViewModel 上的屬性,。Style 的一個重要綁定是在 ListViewItem 的 IsSelected 屬性和 CustomerViewModel 的 IsSelected 屬性之間創(chuàng)建鏈接,,如下所示: XML
選擇或取消選中CustomerViewModel時,會導(dǎo)致所有選定客戶的總銷售額的總和更改,。AllCustomersViewModel 類負責(zé)維護該值,,以便 ListView 下方的 ContentPresenter 可以顯示正確的數(shù)字。圖 17顯示了 AllCustomersViewModel 如何監(jiān)視每個客戶的選擇或取消選擇,,并通知視圖它需要更新顯示值,。 圖 17 監(jiān)控選中或未選中 C#
UI 綁定到 TotalSelectedSales 屬性并對值應(yīng)用貨幣(貨幣)格式。ViewModel 對象可以通過從 TotalSelectedSales 屬性返回 String 而不是 Double 值來應(yīng)用貨幣格式而不是視圖,。ContentPresenter 的 ContentStringFormat 屬性是在 .NET Framework 3.5 SP1 中添加的,,因此如果您必須針對舊版本的 WPF,則需要在代碼中應(yīng)用貨幣格式: XML
包起來WPF 可以為應(yīng)用程序開發(fā)人員提供很多東西,,而學(xué)習(xí)利用這種能力需要轉(zhuǎn)變思維方式,。模型-視圖-視圖模型模式是一組簡單而有效的設(shè)計和實現(xiàn) WPF 應(yīng)用程序的指南。它允許您在數(shù)據(jù),、行為和表示之間創(chuàng)建一個強有力的分離,,從而更容易控制軟件開發(fā)的混亂。 我要感謝 John Gossman 對本文的幫助,。 Josh Smith在 WPF 的出色工作和用戶體驗中,, Josh Smith喜歡在 WPF 的工作和使用體驗。歷史,,和朋友一起探索紐約市,。您可以在 joshsmithonwpf.訪問 Josh 的博客。 |
|
來自: goodwangLib > 《WPF》