本文是翻譯大牛Josh Smith的文章,WPF Apps With The Model-View-ViewModel Design Pattern,譯者水平有限,若有什麼問題請看原文,或者與譯者討論(很是樂意與你討論)。數據庫
本文討論的內容:編程
WPF與設計模式、MVP模式、對WPF來講爲何MVVM是更好的選擇、用MVVM構建WPF程序、本文涉及的技術、WPF、數據綁定。設計模式
目錄:多線程
1引言架構
2有序與混亂app
3模型-視圖-視圖模型的演變框架
4爲何WPF開發者喜歡MVVMide
5演示程序函數
5.1中繼命令邏輯
5.2ViewModel類層級結構
5.3ViewModelBase類
5.4CommandViewModel類
5.5MainWindowViewModel類
5.6View對應ViewModel
5.7數據模型和Repository
5.8新增客戶數據表單
5.9全部客戶視圖
6總結
開發UI,對一個專業軟件並不容易。它須要未知數據、交互式設計,可視化設計、聯通性,多線程、國際化、驗證、單元測試以及其餘的一些東西才能完成。考慮到UI要展現開發的系統而且必須知足用戶對系統風格不可預知的變動,所以它是不少應用程序最脆弱的地方。
有不少的設計模式能夠幫助解決UI不斷變動這頭難纏的野獸,可是恰當的分離和描述多個關注點可能很困難。模式越複雜,以後用到的捷徑越可能破壞以前正確的努力。
這並不老是設計模式的錯。有時使用要寫不少的代碼複雜設計模式,這是由於咱們使用的UI平臺並不適合簡單是設計模式。UI平臺須要作的是很容易使用簡單的,久經考驗的,開發者認識的設計模式構建UI。慶幸的是,WPF就是這樣一個平臺。
隨着是使用WPF開發的比例不斷升高,WPF社區發展了本身的模式與實踐生態圈子。在本文,我將討論一些設計與實現客戶端應用程序的WPF最佳實踐。利用WPF和MVVM設計模式銜接的一些核心功能,我將經過一個例子介紹,用「正確」的方式構建一個WPF程序是多麼的簡單。
data templates, commands, data binding, the resource system以及 MVVM 模式怎麼揉合到一塊兒建立一個簡單的、可測試的、健壯的框架,而且任何WPF程序都能使用,到文章最後,這一切都很清晰明瞭。文中的例程能夠做爲現實中一個WPF應用程序的模版,而且使用MVVM設計模式做爲其核心架構。例程解決方案中的單元測試部分,展現了測試ViewModel類的功能是很容易的。在深刻本文以前,咱們首先看一下咱們要使用像MVVM這樣的設計模式。
沒有必要在一個」Hello,World!」的程序中使用設計模式。任何一個合格的開發者看一眼就指導那幾行代碼是幹什麼的。然而隨着程序功能點的增長,隨之代碼的數量以及移動部件也會增多。最終系統的複雜度以及不斷出現問題,促使開發者組織他們的代碼,以便它們更容易理解,討論、擴展以及維護。咱們經過給代碼中某些實體命以衆所周知的名字,減小複雜系統認知誤區。咱們給函數塊命名主要依據系統中的功能角色。
開發者有意識的根據設計模式組織他們的代碼,而不是根據設計模式自動去組織。不管哪種,都沒有什麼問題。可是在本文中,我說明在WPF程序中明確使用MVVM模式的好處。
某些類的名稱,包括MVVM模式中著名的術語,若是類是View的抽象類就以ViewModel結束。這種方式有助於避免以前提到的認知誤區。相反,你也可讓那種受控的誤區存在,這正是大部分軟件開發項目的自熱狀態。
自從人們開始構建UI時,就有不少流行的設計模式讓UI構建更容易。好比,MVP模式在各類UI編程平臺中都很是流行。MVP是MVC模式的一種變體,MVC模式已經流行了幾十年了。以防你以前從沒用過MVP模式,這裏作一個簡單的解釋。你在屏幕上看到的是View,它顯示的數據是Model,Presenter就是把二者聯繫起來。View依賴Presenter並經過Presenter展現Model數據,響應用戶輸入,提供數據驗證(或許委託給Model去完成)以及其餘的一些任務。若是你想了解更過關於MVP模式,我建議你去讀Jean-Paul Boodhoo的August 2006 Design Patterns column。
2004年晚些時候,Martin Fowler發表了一篇叫Presentation Model(PM)的模式。PM模式和MVP相似,MVP是把一個View從行爲和狀態分離出來。PM中使人關注的部分是建立view的抽象,叫作Presentation Model。以後,View就僅僅是Presentation Model的展現了。在Fowler的論文中,他展現了Presentation Model常常更新View,以便兩個彼此同步。同步邏輯組做爲代碼存在於Presentation Model類中。
2005年,John Gossman,目前是微軟WPF和Silverlight架構師,在他的博客上披露了Model-View-ViewModel (MVVM)模式。MVVM和Fowler的Presentation Model是一致的,兩個模式的特徵都是View的抽象,都包含了View的行爲和狀態。Fowler引入Presentation Model是做爲建立獨立平臺的View的抽象,而Gossman引入MVVM是做爲標準化的方法,利用WPF的核心特色去簡化UI的建立。從這種意義上來說,我把MVVM做爲通常PM模式的一個特例。
在Glenn Block一遍優秀的文章"Prism: Patterns for Building Composite Applications with WPF",於2008年9月微軟大會發布,他解釋了WPF微軟組合程序開發嚮導。術語ViewModel沒有用到,然而PM卻用來描述View的抽象。這篇文章自始至終,都沒沒有出現我要將MVVM模式,以及View的抽象ViewModel。我發現這個術語在WPF和Silverlight社區中比較流行。
不像MVP中的Presenter,ViewModel不須要引用View。View 綁定ViewModel的屬性,ViewMode向Viewl暴露Model對象的數據以及其餘的狀態。View和ViewModel之間的綁定很容易構造,由於ViewModel對象能夠設置爲View的DataContext。若是ViewModel中的屬性值發生改變,新值將經過綁定自動傳送給View。當用戶點擊View中的按鈕時,ViewMode對於的Command將執行請求的動做。ViewModel,毫不是View,去執行實體對象的修改。
View類並不知道Model類是否存在,同時ViewModel和Model也不知道View。實際上,,Model徹底不知道ViewModel和View存在,這是一個很是鬆耦合的設計,在不少方面都有好處,這不就你就會看到。
一旦開發者適應了WPF和MVVM,就很難區別二者。由於MVVM很是適合WPF平臺,而且WPF被設計使用MVVM模式更容易構建應用程序,MVVM就成了WPF開發者的通用語。事實上,微軟內部正在用MVVM開發WPF應用程序,像Microsoft Expression Blend,然而,當時WPF平臺的核心功能依然在開發之中。WPF的不少方面,像控制模型以及數據模版,都利用了MVVM推薦的顯示狀態和行爲分離技術。
MVVM之因此成爲一個偉大設計模式,是由於WPF的一個最重要的特徵數據綁定構造。經過把Viewde 屬性綁定到ViewModel,你就能夠獲得二者鬆耦合的設計,而且徹底去除ViewModel更新View的那部分代碼。數據綁定系統支持輸入驗證,而且輸入驗證提供了傳遞錯誤給View的標準方法。
另兩個WPF的特色,數據模版和資源系統讓MVVM模式更加可用。數據模版把View應用在ViewModel對象上,以便其可以在UI上顯示。你能夠在Xaml中聲明模版,讓資源系統在系統運行過程當中自動定位並應用這些模版。你能夠從我2008年7月寫的一篇文章, "Data and WPF: Customize Data Display with Data Binding and WPF.",獲取更多關於綁定和數據模版的信息。
要不是WPF對Command的支持,MVVM模式就不會那麼強大。本文中,我會爲你展現ViewModel怎樣把Commands暴露給View,而且讓View消費它的功能。若是你對Command不是很熟悉,我推薦你讀一下2008年9月Brian Noyes發佈的文章, "Advanced WPF: Understanding Routed Events and Commands in WPF"。
除了WPF(Silverlight2)自己讓MVVM以一種天然的方式去構建程序以外,形成MVVM模式流行還有一個緣由,那就是ViewModel類很容易進行單元測試。從某種意義來說,View和單元測試只是ViewModel兩個不一樣類型的消費者。擁有一套應用程序的單元測試,能夠爲提供更自由、快速的迴歸測試,而回歸測試有助於下降以後應用的維護成本。
除了促進建立自動化迴歸測試外,ViewModel類的可測試性也有助於設計更容易分離的UI。當你設計應用時,你能夠經過想象某些東西是否要建立單元測試消費ViewModel,來肯定它們是放到View裏面仍是ViewModel裏面。若是你能夠爲ViewModel寫單元測試而不用建立任何UI控件,你也能夠把ViewModel剝離出來,由於它不依賴任何具體可視化的組件。
最後,對於要和設計者合做的開發者來講,使用MVVM模式使得建立平滑的開發/設計工做流更加容易。既然View能夠是ViewModel的任意一個消費者,就很容易去掉一個View經過新增一個View去渲染ViewModel。這個簡單的步驟容許設計師構建快速原型以及評估UI設計。
這樣開發團隊能夠關注建立健壯的ViewModel類,而設計團隊能夠關注設計界面友好的View。要融合兩個團隊輸出只須要在View的xaml上進行正確的綁定便可。
到此爲止,咱們回顧了MVVM的歷史以及具體操做理論。我也說明了它在WPF開發者中間如此流行的緣由。如今是時候繼續咱們的步伐,看一下MVVM模式在實際中的應用。這篇文章中的演示程序以各類方式使用MVVM設計模式,它提供了豐富的例子,幫助在上下文中理解MVVM的概念。我用VS2008 SP1建立的這個演示程序, 框架是Microsoft .NET Framework 3.5 SP1。單元測試是用的Visual Studio unit testing。
應用能夠包含任意數量的「Workspace」,每個均可以由用戶點擊左側導航區的命令連接打開。全部的Workspace寄宿在主區域TabControl中,用戶能夠經過點擊Workspace的 tab item上關閉按鈕關閉Workspace。應用程序有兩個可用的Workspace:"All Customers" 和 "New Customer"。運行程序,打開一些Workspace,UI看起來如圖1所示。
圖1 Workspaces
一次只有一個All Customers Workspace的實例能夠打開,可是能夠打開多個New Customer Workspace。當用戶決定建立一個新客戶時,她必須填完圖2所示的數據輸入表單。
圖2 新客戶數據輸入表單
填完數據輸入表單的全部有效值點擊「Save」按鈕,新客戶的名稱將會出如今tab item 上面,同時新客戶也會增長到客戶列表中。應用程序不支持刪除或者編輯客戶,可是這和其它功能相似,很容易在已有的程序架構上去實現。如今你已經對演示程序有了更深層次的理解了,接下來咱們研究它是如何設計以及實現的。
除了類構造器裏調用初始化組件標準的樣板代碼,應用中的每一View的code-behind文件都是空的。實際上你能夠移除View的code-behind文件,程序讓人可以爭正確的編譯和運行。儘管View中沒有事件處理方法,可是當用戶點擊按鈕時,程序依然可以響應並知足用戶的請求。之因此這樣,是由於UI上Hyperlink、 Button以及MenuItem控件的Command屬性被綁定了。綁定機制確保當用戶在控件上點擊時,由ViewModel暴露的ICommand對象可以執行。你能夠把command對象看做一個適配器,這個適配器讓command對象很容易消費在View中聲明的ViewModel功能。
當ViewModel暴露ICommad類型的實例屬性,被暴露的Command對象使用ViewModel中的對象去完成它的工做。其中一個可能的實現模式是在ViewModel內建立一個私有嵌套類,以便command可以訪問包含在ViewModel中的私有成員,而不至於污染命名空間。嵌套類實現了ICommand接口,包含在ViewModel中對象的引用注入到其構造器中。可是爲ViewModel暴露的每一個Command建立實現ICommad的嵌套類,會增長ViewModel類的大小。更多的代碼意味着存在BUGS潛力更大。
在演示程序中,RelayCommand類解決了這個問題。RelayCommand容許經過把委託傳給其構造器,以實現對命令邏輯的注入。這種方式容許在ViewMode類中能夠簡單明瞭的實現Command。
RelayCommand是DelegateCommand的一個簡單的變體,DelegateCommand能夠在Microsoft Composite Application Library找到。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
}
做爲接口ICommad實現一部分,事件CanExecuteChanged有一些值得關注的特徵。它委託訂閱CommandManager. RequerySuggested事件。這樣以確保不管什麼時候調用內置命令時,WPF命令架構都能調用全部可以執行的RelayCommand對象。
RelayCommand _saveCommand;
public ICommand SaveCommand
{
get
{
if (_saveCommand == null)
{
_saveCommand = new RelayCommand(param => this.Save(),
param => this.CanSave );
}
return _saveCommand;
}
}
大部分ViewModel類有共同的特徵,他們要實現INotifyPropertyChanged接口,須要顯示一個友好的名字,以以前說道Workspace爲例,它須要可以關閉(即從UI上移除)。要解決這個問題,天然就須要建立一個或二個ViewModel基類,以便新的ViewModel類可以從基類集成通用的功能。全部的ViewModel類造成如圖4的層級圖。
圖4 繼承層級圖
爲你的ViewModel建立一個基類並非必須。若是你喜歡在類中經過組合幾個小一點的類以得到那些功能,而不是用繼承的方式,這並無什麼問題。就像任何其餘的設計模式同樣,MVVM是一套指導方針,而不是規則。
ViewModelBase 是層級中的根類,這就是它要實現通用INotifyPropertyChanged接口以及有一個DisplayName屬性的緣由。INotifyPropertyChanged接口包含一個叫PropertyChanged的事件。不管什麼時候ViewModel對象的屬性的發生改變時,它都會觸發PropertyChanged事件,把新值通知給WPF綁定系統。根據通知,綁定系統檢索屬性,UI組件上綁定的屬性將接受新值。
爲了讓WPF知道是那一個屬性發生了改變,PropertyChangedEventArgs類暴露了一個string類型的屬性PropertyName 。你必定要爲事件參數傳遞正確的屬性名,不然WPF將會爲新值檢索出一個錯誤的屬性。
ViewModelBase一個值得關注的地方就是它爲給定的屬性名提供了驗證,驗證屬性是否存在ViewModel對象上。重構時,這很是有用。由於經過VS 2008重構功能去改變屬性名,不會更新源代碼中字符串,而這些字符串正好包含屬性名(其實不該該包含)。在事件參數中傳遞不正確的屬性名,觸發PropertyChanged事件時,可能會致使微小的BUGs,而且這些BUGs很難追蹤,所以這個細微的特徵將會節省大量的時間。ViewModelBase中增長了這個有用的特徵,其代碼以下:
圖5 屬性驗證
// In ViewModelBase.cs
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
[Conditional("DEBUG")]
[DebuggerStepThrough]
public void VerifyPropertyName(string propertyName)
{
// Verify that the property name matches a real,
// public, instance property on this object.
if (TypeDescriptor.GetProperties(this)[propertyName] == null)
{
string msg = "Invalid property name: " + propertyName;
if (this.ThrowOnInvalidPropertyName)
throw new Exception(msg);
else
Debug.Fail(msg);
}
}
CommandViewModel是最簡單的ViewModelBase子類,它暴露了一個類型爲ICommad的Command屬性。MainWindowViewModel經過Commands屬性暴露了CommandViewModel對象的一個集合。主窗口左手側的導航區域,顯示了MainWindowViewModel暴露每一個CommandViewModel對象連接,像「View all customers」和「Create new customer」。當用戶點擊連接,將會執行相應的Command,在主窗口的TabControl中打開一個workspace。CommandViewModel類的定義以下所示:
public class CommandViewModel : ViewModelBase
{
public CommandViewModel(string displayName, ICommand command)
{
if (command == null)
throw new ArgumentNullException("command");
base.DisplayName = displayName;
this.Command = command;
}
public ICommand Command { get; private set; }
}
在MainWindowResources.xaml文件中存在一個key爲CommandsTemplate的數據模版,主窗口(MainWindow)使用這個模版渲染以前提到的CommandViewModel對象集合。這個模版是簡單在ItemsControl裏把每一個CommandViewModel對象渲染成一個連接,每一個連接的Command屬性綁定到CommandViewModel對象的Command屬性。數據模版Xaml如圖6所示:
圖6 渲染Command列表
<!-- In MainWindowResources.xaml -->
<!--
This template explains how to render the list of commands on
the left side in the main window (the 'Control Panel' area).
-->
<DataTemplate x:Key="CommandsTemplate">
<ItemsControl ItemsSource="{Binding Path=Commands}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Margin="2,6">
<Hyperlink Command="{Binding Path=Command}">
<TextBlock Text="{Binding Path=DisplayName}" />
</Hyperlink>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
如前面看到的類圖同樣,WorkspaceViewModel類繼承於ViewModelBase並增長了「關閉」的能力。這個「關閉」,個人意思是在運行的時候能把workspace從UI上移除。有三個類繼承於WorkspaceViewModel,他們分別爲MainWindowViewModel,AllCustomersViewModel和CustomerViewModel。MainWindowViewModel的關閉請求是由App類處理的,其中App類建立了MainWindow以及它對應的ViewModel對象。建立代碼如圖7所示.
圖7 建立ViewModel
// In App.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainWindow window = new MainWindow();
// Create the ViewModel to which
// the main window binds.
string path = "Data/customers.xml";
var viewModel = new MainWindowViewModel(path);
// When the ViewModel asks to be closed,
// close the window.
viewModel.RequestClose += delegate
{
window.Close();
};
// Allow all controls in the window to
// bind to the ViewModel by setting the
// DataContext, which propagates down
// the element tree.
window.DataContext = viewModel;
window.Show();
}
MainWindow包含一個菜單項,該菜單項的Command屬性綁定到MainWindowViewModel上的CloseCommand屬性上。當用戶點擊該菜單,App類響應請求,調用窗體的關閉方法。菜單Xaml以下所示:
<!-- In MainWindow.xaml -->
<Menu>
<MenuItem Header="_File">
<MenuItem Header="_Exit" Command="{Binding Path=CloseCommand}" />
</MenuItem>
<MenuItem Header="_Edit" />
<MenuItem Header="_Options" />
<MenuItem Header="_Help" />
</Menu>
MainWindowViewModel包含了WorkspaceViewModel對象一個observable類型的集合,該集合的名稱爲Workspaces。主窗體包含了一個TabControl,其ItemsSource綁定到上述的集合。每個tab item都有一個關閉按鈕,其Command屬性綁定到它對應WorkspaceViewModel實例的CloseCommand上。模版展現瞭如何渲染一個帶關閉按鈕的tab item。配置tab item模版的簡化版會展現在下面代碼中,這段代碼能夠在MainWindowResources.xaml文件中找到。
<DataTemplate x:Key="ClosableTabItemTemplate">
<DockPanel Width="120">
<Button
Command="{Binding Path=CloseCommand}"
Content="X"
DockPanel.Dock="Right"
Width="16" Height="16"
/>
<ContentPresenter Content="{Binding Path=DisplayName}" />
</DockPanel>
</DataTemplate>
當用戶點擊tab item上的關閉按鈕時,會執行WorkspaceViewModel的CloseCommand,觸發它的RequestClose事件。MainWindowViewModel會監控workspace的RequestClose事件,根據請求從Workspaces集合中移除相應的workspace。由於MainWindow的TabControl的ItemsSource綁定到WorkspaceViewModel的observable集合,從集合中移除對象,會引發從TabControl中移除相應的workspace。MainWindowViewModel相應的邏輯如圖8所示
圖8 從UI上移除workspace
// In MainWindowViewModel.cs
ObservableCollection<WorkspaceViewModel> _workspaces;
public ObservableCollection<WorkspaceViewModel> Workspaces
{
get
{
if (_workspaces == null)
{
_workspaces = new ObservableCollection<WorkspaceViewModel>();
_workspaces.CollectionChanged += this.OnWorkspacesChanged;
}
return _workspaces;
}
}
void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count != 0)
foreach (WorkspaceViewModel workspace in e.NewItems)
workspace.RequestClose += this.OnWorkspaceRequestClose;
if (e.OldItems != null && e.OldItems.Count != 0)
foreach (WorkspaceViewModel workspace in e.OldItems)
workspace.RequestClose -= this.OnWorkspaceRequestClose;
}
void OnWorkspaceRequestClose(object sender, EventArgs e)
{
this.Workspaces.Remove(sender as WorkspaceViewModel);
}
在UnitTests項目中,MainWindowViewModelTests.cs文件包含了一個測試方法,該方法驗證上述功能是否正確執行。很容易爲ViewModel類建立單元測試是MVVM模式的一個大賣點,由於它只需測試應用程序的功能,而不用寫和UI交互的代碼。上述測試方法圖9所示
圖9 測試方法
// In MainWindowViewModelTests.cs
[TestMethod]
public void TestCloseAllCustomersWorkspace()
{
// Create the MainWindowViewModel, but not the MainWindow.
MainWindowViewModel target =
new MainWindowViewModel(Constants.CUSTOMER_DATA_FILE);
Assert.AreEqual(0, target.Workspaces.Count, "Workspaces isn't empty.");
// Find the command that opens the "All Customers" workspace.
CommandViewModel commandVM =
target.Commands.First(cvm => cvm.DisplayName == "View all customers");
// Open the "All Customers" workspace.
commandVM.Command.Execute(null);
Assert.AreEqual(1, target.Workspaces.Count, "Did not create viewmodel.");
// Ensure the correct type of workspace was created.
var allCustomersVM = target.Workspaces[0] as AllCustomersViewModel;
Assert.IsNotNull(allCustomersVM, "Wrong viewmodel type created.");
// Tell the "All Customers" workspace to close.
allCustomersVM.CloseCommand.Execute(null);
Assert.AreEqual(0, target.Workspaces.Count, "Did not close viewmodel.");
}
MainWindowViewModel間接從主窗體的TabControl控件中增長移除WorkspaceViewModel對象。經過數據綁定,TabItem的Content屬性顯示繼承於ViewModelBase的對象。ViewModelBase並非一個UI元件,所以他並不支持渲染它本身。默認在TextBlock中,WPF的一個非可視化對象經過調用ToString方法以顯示該對象。很明顯這不是你想要的,除非你的用戶迫切的想知道ViewModel的類型名。
咱們經過強類型數據模版很容易告訴WPF如何渲染ViewModel對象。強類型數據模版key屬性名沒有賦值,可是其DataType屬性要賦以類型類的實例。若是WPF要去渲染ViewModel對象,它會檢查在資源系統範圍內是否有一個強類型數據模版的DataType和ViewModel對象(或者其基類)的類型同樣。若是找到一個這樣的模版的話,他會用該模版去渲染被TabItem Content屬性綁定的ViewModel對象。
MainWindowResources.xaml文件中有一個ResourceDictionary(資源字典),該字典被增長到主窗體的資源層級中,這意味着文件包含的資源在正窗體範圍內有效。當一個TabItem的Content屬性設置ViewModel對象時,該字典中的強類型數據模版會提供一個View(即用戶自定義控件)去渲染TabItem Content。具體如圖10所示
圖10 提供View
<!--
This resource dictionary is used by the MainWindow.
-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:DemoApp.ViewModel"
xmlns:vw="clr-namespace:DemoApp.View"
>
<!--
This template applies an AllCustomersView to an instance
of the AllCustomersViewModel class shown in the main window.
-->
<DataTemplate DataType="{x:Type vm:AllCustomersViewModel}">
<vw:AllCustomersView />
</DataTemplate>
<!--
This template applies a CustomerView to an instance
of the CustomerViewModel class shown in the main window.
-->
<DataTemplate DataType="{x:Type vm:CustomerViewModel}">
<vw:CustomerView />
</DataTemplate>
<!-- Other resources omitted for clarity... -->
</ResourceDictionary>
你不須要寫任何代碼去決定哪個View去展現ViewModel對象。WPF資源系統把你從繁重的工做解脫出來,讓你去關注更重要的事情。在複雜的場景中,可能須要經過編程去選擇View,可是在大部分狀況下,經過編程選擇View是沒必要要的。
你已經知道應用程序如何去加載,顯示以及關閉一個ViewModel對象。如今一切已經就位,你能夠在整個應用程序範圍內,回顧一下具體實現的細節。在深刻理解應用程序的兩個workspace,「All Customers」 和 「New Customer」以前,咱們先審視一下數據模型和數據存取類。這些類的設計和MVVM模式並無什麼關係,由於你能夠建立一個ViewModel類,以適應任何對WPF友好的數據對象。
演示程序中惟一的模型類Customer類,該類有些屬性表徵一個公司的客戶,像他們的姓名,email等。它經過實現IDataErrorInfo接口提供屬性驗證信息,該接口在WPF大行其道以前已存在多年。Customer類裏面並無什麼,並非建議其用於MVVM架構,或者甚至應用和WPF應用程序。這個類很容易從遺留的業務庫中獲取。
數據必須存到某個地方,在這個應用程序中,CustomerRepository類的實例加載並存儲全部的Customer對象。該CustomerRepository從xml文件加載全部的客戶數據,這與外部的數據源無關。數據可能來自數據庫、Web服務、命名管道、磁盤上的文件甚至信鴿,這並無什麼關係。只要你有一個有數據.net對象,無論它來自何方,MVVM模式都能在屏幕上獲取其包含數據。
CustomerRepository類暴露了一些方法,這些方法容許你獲取全部的Customer對象,增長一個Customer對象到存儲室並檢查其子啊存儲室是否存在。既然應用程序不容許刪除客戶,存儲室也不容許你去刪除客戶。當一個新Customer經過AddCustomer方法增長到CustomerRepository時,會觸發CustomerAdded事件。
很明顯,和真實業務程序所需的相比,該程序的數據模型是輕量級的,可是這並無關係。重要的是,要理解ViewModel類如何利用Customer和CustomerRepository類。知道CustomerViewModel是對Customer對象的封裝,其經過一系列的屬性暴露了Customer的狀態,以及被CustomerView使用的狀態。CustomerViewModel並非複製Customer對象的狀態,而是經過委託暴露這狀態,具體以下:
public string FirstName
{
get { return _customer.FirstName; }
set
{
if (value == _customer.FirstName)
return;
_customer.FirstName = value;
base.OnPropertyChanged("FirstName");
}
}
當用戶在CustomerView控件中建立新客戶點擊保存按鈕時,與該視圖關聯的CustomerViewModel會增長一個Customer對象到CustomerRepository 。這會觸發存儲庫的CustomerAdded事件,該事件讓AllCustomersViewModel知道他應該增長一個CustomerViewModel對象到AllCustomers集合。從某種意義說,CustomerRepository在各自ViewModel和他們要處理的Customer對象間扮演數據同步的角色,或許有人會把這當成中介者模式。我會在接下來的內容中介紹其實現機理,可是爲了更進一步瞭解這些類如何鏈接在一塊兒,咱們如今先看一下圖11所示的類圖
圖11 Customer關係圖
當用戶點擊「Create new customer」連接,MainWindowViewModel會增長一個CustomerViewModel到workspaces集合,相應的CustomerView回去顯示。用戶在輸入框輸入有效值以後,Save按鈕變爲可用狀態,以便用戶能存儲增長客戶信息。這並無超出常規的地方,只是一個帶有驗證信息和Save按鈕的常規輸入表單而已。
Customer類內置輸入驗證支持,這是經過實現IDataErrorInfo接口得到。輸入驗證確保客戶有一個名字,合法的email地址,若是客戶是我的客戶,還須要姓氏。若是Customer對象的IsCompany屬性爲真,則其LastName屬性不能有值。該驗證邏輯從Customer對象的角度看是有意義的,可是它並不能知足UI的須要,UI要求用戶選擇客戶類別是我的仍是公司。Customer類別選擇器初始值是: (Not Specified),若是Customer對象的IsCompany屬性只容許是true和false,客戶類別是unspecified時,UI如何告訴用戶?
假定你對整個軟件系統擁有控制權限,你能夠把IsCompany屬性類型改變爲Nullable<bool>,該類型容許「未選擇」值(即空值-譯者注)。然而在現實世界中,不是這麼簡單。假設你不能改變Customer類,由於它來自公司其餘團隊所開發系統。要是由於數據庫的緣由,沒有簡單方法存儲未選擇的值,怎麼辦?要是其它程序已經使用Customer類,而且其依賴正常Boolean類型的IsCompany屬性,怎麼辦?諸如此類,可使用ViewModel去解決。
圖12所示的測試方法展現了該功能如何在CustomerViewModel中工做,CustomerViewModel暴露了一個CustomerTypeOptions屬性,以便UI上的客戶類型選擇器有三個字符串顯示。同時它也暴露了一個CustomerType屬性,該屬性存放選擇器選中的字符串。當CustomerType被賦值時,它會潛在的Customer對象IsCompany屬性,把字符類型轉化爲Boolean類型。圖13展現了這兩個屬性。
圖12 測試方法
// In CustomerViewModelTests.cs
[TestMethod]
public void TestCustomerType()
{
Customer cust = Customer.CreateNewCustomer();
CustomerRepository repos = new CustomerRepository(
Constants.CUSTOMER_DATA_FILE);
CustomerViewModel target = new CustomerViewModel(cust, repos);
target.CustomerType = "Company"
Assert.IsTrue(cust.IsCompany, "Should be a company");
target.CustomerType = "Person";
Assert.IsFalse(cust.IsCompany, "Should be a person");
target.CustomerType = "(Not Specified)";
string error = (target as IDataErrorInfo)["CustomerType"];
Assert.IsFalse(String.IsNullOrEmpty(error), "Error message should
be returned");
}
圖13 CustomerTypeOptions和CustomerType
// In CustomerViewModel.cs
public string[] CustomerTypeOptions
{
get
{
if (_customerTypeOptions == null)
{
_customerTypeOptions = new string[]
{
"(Not Specified)",
"Person",
"Company"
};
}
return _customerTypeOptions;
}
}
public string CustomerType
{
get { return _customerType; }
set
{
if (value == _customerType ||
String.IsNullOrEmpty(value))
return;
_customerType = value;
if (_customerType == "Company")
{
_customer.IsCompany = true;
}
else if (_customerType == "Person")
{
_customer.IsCompany = false;
}
base.OnPropertyChanged("CustomerType");
base.OnPropertyChanged("LastName");
}
}
CustomerView用戶控件中有一個ComboBox綁定這兩個屬性,以下所示:
<ComboBox
ItemsSource="{Binding CustomerTypeOptions}"
SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}"
/>
當ComboBox中的選擇項發生改變時,其數據源的將會掃描IDataErroInfo接口查看新值是否有效。之因此這樣,是由於綁定的SelectedItem屬性有一個ValidateOnDataErrors設置爲true。既然數據源是一個CustomerViewModel對象,綁定系統會會向CustomerViewModel對象要求一個對CustomerType屬性的驗證信息。大多狀況下,CustomerViewModel會把全部的驗證請求委託給其包含的Customer對象。然而,由於Customer的IsCompany屬性沒有未選中狀態的概念,因此CustomerViewModel必須對ComboBox中新選擇項進行處理。具體代碼如圖14所示。
圖14 驗證CustomerViewModel對象
// In CustomerViewModel.cs
string IDataErrorInfo.this[string propertyName]
{
get
{
string error = null;
if (propertyName == "CustomerType")
{
// The IsCompany property of the Customer class
// is Boolean, so it has no concept of being in
// an "unselected" state. The CustomerViewModel
// class handles this mapping and validation.
error = this.ValidateCustomerType();
}
else
{
error = (_customer as IDataErrorInfo)[propertyName];
}
// Dirty the commands registered with CommandManager,
// such as our Save command, so that they are queried
// to see if they can execute now.
CommandManager.InvalidateRequerySuggested();
return error;
}
}
string ValidateCustomerType()
{
if (this.CustomerType == "Company" ||
this.CustomerType == "Person")
return null;
return "Customer type must be selected";
}
該部分代碼重點在於CustomerViewModel實現了IDataErrorsInfo接口,能夠處理對CustomerViewModel具體屬性的驗證請求,同時把其它請求委託給Customer對象處理。這樣容許咱們使用Model類的驗證邏輯,其它屬性驗證在ViewModel類中才有意義。
經過SaveCommand屬性去保存CustomerViewModel對象,該命令使用了以前陳述的RelayCommand,容許CustomerViewModel決定其是否能保存本身以及被告知保存其狀態時作什麼。在該程序中,保存一個新客戶只是把其增長到CustomerRepository。決定一個新客戶是否可以保存,須要兩方面的許可,一是Customer對象是否有效,二是CustomerViewModel必須是有效的。這兩方面是必要條件,因爲前面陳述的ViewModel其特定屬性以及Customer對象驗證信息。CustomerViewModel的保存邏輯如圖15所示
圖15 CustomerViewModel的保存邏輯
// In CustomerViewModel.cs
public ICommand SaveCommand
{
get
{
if (_saveCommand == null)
{
_saveCommand = new RelayCommand(
param => this.Save(),
param => this.CanSave
);
}
return _saveCommand;
}
}
public void Save()
{
if (!_customer.IsValid)
throw new InvalidOperationException("...");
if (this.IsNewCustomer)
_customerRepository.AddCustomer(_customer);
base.OnPropertyChanged("DisplayName");
}
bool IsNewCustomer
{
get
{
return !_customerRepository.ContainsCustomer(_customer);
}
}
bool CanSave
{
get
{
return String.IsNullOrEmpty(this.ValidateCustomerType()) &&_customer.IsValid;
}
}
這裏ViewModel的使用使得建立顯示Customer對象的View更加容易,而且容許像Boolean類型未選中這樣事情存在。同時它很容易告訴客戶保存其狀態。若是View直接綁定到Customer對象,View將會須要不少代碼才能恰當的工做。在一個設計良好的MVVM架構中,大部分View的背後代碼應該爲空,或者最多隻包含操縱View內的控件以及資源的代碼。有時在View後面寫一些代碼也是必須的,由於要和ViewModel對象進行交互,像傳遞事件或者調用方法不然從ViewModel作些事情很難。
演示程序也包含了一個在ListView中顯示全部客戶列表的workspace。這些客戶經過根據其是我的客戶仍是公司客戶進行分組。用戶一次能夠選擇一個或者多個客戶,在右下方查看其總銷售額。
該UI是AllCustomersView控件,用以渲染AllCustomersViewModel對象。每一個ListViewItem表明一個CustomerViewModel對象,該對象存在於AllCustomerViewModel對象暴露的AllCustomers集合中。在前一部分,你看到CustomerViewModel如何渲染成數據輸入表單,而如今如出一轍的CustomerViewModel對象卻被渲染成ListView中的一個Item。CustomerViewModel並不知道那一個可視化的組件去顯示它,這使得其重用成爲可能。
AllCustomersView建立了在ListView中看到的分組,這是經過把ListView的ItemsSource綁定到配置如圖16所示的CollectionViewSource中實現的。
圖16 CollectionViewSource
<!-- In AllCustomersView.xaml -->
<CollectionViewSource
x:Key="CustomerGroups"
Source="{Binding Path=AllCustomers}"
>
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="IsCompany" />
</CollectionViewSource.GroupDescriptions>
<CollectionViewSource.SortDescriptions>
<!--
Sort descending by IsCompany so that the ' True' values appear first,
which means that companies will always be listed before people.
-->
<scm:SortDescription PropertyName="IsCompany" Direction="Descending" />
<scm:SortDescription PropertyName="DisplayName" Direction="Ascending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
ListViewItem和CustomerViewModel之間的關聯是經過ListView的ItemContainerStyle屬性創建的。指定給該屬性的Style應用於每一個ListViewItem,這使得ListViewItem的屬性能夠綁定到CustomerViewModel對象的屬性上。這個Style一個重要的綁定就是在ListViewItem的IsSelected屬性和CustomerViewModel的IsSelected屬性之間創建的聯繫,以下所示:
<Style x:Key="CustomerItemStyle" TargetType="{x:Type ListViewItem}">
<!-- Stretch the content of each cell so that we can
right-align text in the Total Sales column. -->
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<!-- Bind the IsSelected property of a ListViewItem to the IsSelected property of a CustomerViewModel object.-->
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
</Style>
當CustomerViewModel對象被選中仍是未選中,會引發全部選中客戶銷售總額發生改變。AllCustomersViewModel負責維護總銷售額,以便ListView下部的ContentPresenter顯示正確的數字。圖17顯示AllCustomersViewModel如何監控被選中或未選中的每一個客戶,並通知View更新要顯示的值。
圖17 監控選中或未選中的客戶
// In AllCustomersViewModel.cs
public double TotalSelectedSales
{
get
{
return this.AllCustomers.Sum(custVM=>custVM.IsSelected ? custVM.TotalSales : 0.0);
}
}
void OnCustomerViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
string IsSelected = "IsSelected";
// Make sure that the property name we're
// referencing is valid. This is a debugging
// technique, and does not execute in a Release build.
(sender as CustomerViewModel).VerifyPropertyName(IsSelected);
// When a customer is selected or unselected, we must let the
// world know that the TotalSelectedSales property has changed,
// so that it will be queried again for a new value.
if (e.PropertyName == IsSelected)
this.OnPropertyChanged("TotalSelectedSales");
}
UI綁定了TotalSelectedSales屬性,並把該值置爲貨幣格式。ViewModel對象,而不是View,經過返回TotalSelectedSales屬性Double類型值的字符串形式,設置其貨幣格式。.NET Framework 3.5 SP1 爲ContentPresenter增長了ContentStringFormat屬性,若是你使用更老版本的WPF,你須要在代碼中設置貨幣格式。
<!-- In AllCustomersView.xaml -->
<StackPanel Orientation="Horizontal">
<TextBlock Text="Total selected sales: " />
<ContentPresenter
Content="{Binding Path=TotalSelectedSales}"
ContentStringFormat="c"/>
</StackPanel>
WPF爲應用程序開發者提供了不少,學習利用WPF賦予的力量,但須要轉變思惟模式。MVVM模式是設計和開發WPF程序的一種簡單而又有效的一套指導方針。它容許你建立數據、行爲和展現強分離的程序,這更容易控制軟件開發中的混亂因素。