[译]WPF 应用程序和MVVM设计模式 ——Josh Smith
這篇文章討論:
模式與WPF
MVP模式
為什么MVVM更加適用于WPF
用MVVM構建一個應用程序
譯文地址:
? ? ? ??http://www.cnblogs.com/lujiao_cs/archive/2011/10/30/2229419.html
內容:
專業的軟件用戶界面開發不太簡單。它可能混雜了數據、交互設計、視覺設計、連接、多線程、安全性、國際化、驗證、單元測試以及可觸摸技術。考慮到用戶界面暴露了系統的底層并且必須滿足用戶的不可預知的需求,它可能是許多應用程序中最不穩定的部分。有一些常用的設計模式可以解決這個問題,但是隔離并且訪問這些關注點可能比較難。模式越是復雜,更多的捷徑可能會被用到,這些捷徑漸漸的破壞了以前所有正確方式做事情的努力。
不總是設計模式的問題。有時候我們會用到復雜的設計模式,由于現有的UI平臺不能提供一個很好的設計模式,它需要寫很多的代碼。我們需要的是這樣一種平臺,它使得利用簡單的、能夠經受時間考驗的、并且能讓開發者接受的設計模式來構建UI變得簡單。幸運的是,WPF很好的提供了這些。
由于軟件界采用WPF的比率不斷增長,WPF團隊已經開發了它自己的模式生態系統和實踐。在這篇文章中,我將回顧一些很好的設計實踐,并且用WPF實現一個應用程序客戶端。利用WPF的一些核心特性結合MVVM設計模式,我將介紹一個實例程序,它展示了利用正確的方式構建WPF程序是多么簡單。
在這篇文章的結尾,我們將用數據模板、命令、數據綁定、資源系統和MVVM模式結合在一起創建一個簡單、可測試的、簡單的框架。利用這個框架,我們可以創建任何的WPF程序。此文章的示例程序可以當做一個真實的WPF項目的模板,它運用MVVM作為核心架構。程序中的單元測試項目會告訴你測試應用程序的UI功能是多么簡單,這些功能存在于ViewModel類中。在進入細節之前,讓我們首先回顧一下為什么要用MVVM這樣的模式。
有序與混亂
在一個簡單的“hello?world”程序里面使用設計模式是多余的,并且會適得其反。任何一個合格的開發人員能一目了然的理解幾行代碼。?然而,當程序的功能增加時,代碼行和組件的數量相應地也會增加。最后,系統的復雜性和重復出現的問題增加,這促使開發者以一種易于理解、討論、擴展、故障排除的的方式去組織代碼。在源代碼中,我們通過對實體的良好命名來減少認知的混亂。我們通過考慮它在系統中的功能角色來確定適用于一段代碼的名稱。
開發者經常通過設計模式特意的組織代碼,而不是自然的運用它們。這兩種方式都沒有錯,但是在這篇文章里,我考察了在一個WPF應用程序中明確地使用MVVM作為架構的好處。這些類的名字包含了MVVM設計模式中熟悉的條款。例如,以“ViewModel”結尾的類是View的抽象。這有助于減少避免前面提到的認知的混亂。你能很愉快的控制這種混亂,在許多專業的軟件開發項目中這是一個自然的狀態。
MVVM的演化
自從人們開始創建軟件的用戶界面,就有一些常用的設計模式使它變得變得簡單。例如,MVP模式在各種UI編程平臺下大受歡迎。MVP?是MVC模式的變種,它已經存在了數十年了。如果你以前沒有用過MVP模式,這里做一個簡單的說明。你在屏幕上看到的就是VIew,它所展示的數據就是Model,Presenter連接這兩者。View依賴于Presenter?利用Model的數據去填充它、對用戶的輸入做出反應、提供輸入驗證以及一些其它的驗證。如果你想要學習更多的MVP,推薦你閱讀Jean-Paul?Boodhoo?的?August?2006?Design?Patterns?column。在2004年,Martin?Fowler?發表了關于Presentation?Model?(PM)模式的文章。PM?模式在將View和他的狀態和行為分離上面比較相似。PM模式有趣的地方是創造了名為Presentation?Model的View的抽象。View僅僅只是Presentation?Model的表現。按照Fowler的解釋,Presentation?Model頻繁更新的是它的View,以便這兩者之間保持一致。?同步的邏輯作為代碼存在于Presnetation?Model類里。
在2005年,微軟的WPF和SL架構師John?Gossman在他的博客中公開了MVVM模式。?MVVM與?Fowler的PM模式完全相同,這兩種模式都專注于View的抽象,它包含了View的狀態和行為。Fowler引入Presentation?Model作為一種與UI平臺無關的View的抽象的創建,而Gossman引入MVVM作為一種利用WPF的核心特征去簡化用戶界面的創建的標準化的方式。?在這個以意義上看,MVVM相對于通用的PM更加專一化,是為WPF和Silverlight平臺而量身定做的。?
Glenn?Block在2008年九月發表了這篇優秀的文章:Prism:?Patterns?for?Building?Composite?Applications?with?WPF。他解釋了針對于WPF的Microsoft?Composite?Application?Guidance。ViewModel?還未被用到。反而,Presentation?Model?被用來描述View的抽象。貫穿這篇文章,我將這種模式叫做MVVM、View的抽象叫做ViewModel。我發現這個術語在WPF和MVVM社區更加流行。
不像MVP的提出,ViewModel?不需要View的引用。View綁定到ViewModel中的一個屬性。這個屬性展示了Model對象的數據以及View的其它狀態特性。由于ViewModel?對象被設置為View的DataContent,View和ViewModel之間的綁定易于構建。如果ViewModel?里面的屬性值改變了,那些新的值會通過數據綁定傳遞到View。當用戶點擊View的一個按鈕,ViewModel?里面的一個命令就會執行完成需要的動作。ViewModel而不是View執行了Model數據的修改。View類不知道Model類的存在,ViewModel?和Model也不知道View。實際上,Model完全不知道ViewModel和View存在的事實。?這是一種松耦合的設計,你將看到這種方式的好處。
為什么WPF開發者喜歡MVVM
一旦開發人員喜歡上了MVVM和WPF,就很難區分他們兩者。MVVM?是WPF開發者的通用語,因為他非常適合WPF平臺,WPF被設計使它很容易使用MVVM模式去構建應用程序。實際上,微軟內部利用MVVM開發應用程序,例如Expression?Blend,而核心WPF平臺正在構建中。WPF的許多方面,例如look-less控件模型和數據模板,通過MVVM將顯示和狀態和行為分離。使得MVVM成為一種優秀的設計模式的最重要的一個方面就是數據綁定。通過將View的屬性綁定到ViewModel,可以在這兩者之間得到松耦合,完全地移除了在ViewModel里直接寫代碼更新一個view的需要。數據綁定系統同時提供了輸入驗證,提供了一個標準的方式將驗證錯誤傳到View。
WPF的另外兩個使得MVVM模式有用的特性的數據模板和資源系統。View的數據模板將ViewModel的對象顯示到用戶界面。你可以在Xaml中定義模板,并且在運行時讓資源系統自動查找和應用這些模板。你可以在我的2008年7月的文章Data?and?WPF:?Customize?Data?Display?with?Data?Binding?and?WPF中學習更多的數據綁定和數據模板。如果WPF不支持Commands,MVVM將遠沒有如此強大。在這篇文章中,我將告訴你ViewModel如何將Commands公開到View,從而讓View使用它的功能。如果你對Command不熟悉,我推薦你閱讀Brian?Noyes在2008年9月的綜合性的文章Advanced?WPF:?Understanding?Routed?Events?and?Commands?in?WPF。
除了WPF(以及Silverlight2)的特征使得MVVM自然地方式去構建應用程序,這個模式流行的也因為ViewModel類容易做單元測試。當一個程序的交互邏輯存在于一套ViewModel類中時,你可以很容易寫出代碼測試它。從某種意義上說看,Views?和單元測試是ViewModel?的兩種不同的消費者。應用程序的ViewModel的一套測試程序提供了一個自由快速的回歸測試,幫助減少程序維護的消耗。
除了促進創造自動回歸測試,ViewModel?的可測試性有利于正確地設計易于換膚的用戶界面。當你設計一個應用程序的時候,想象你需要你需要寫一個單元測試來測試ViewModel,這樣你就明白一些功能是該寫在View里面還是ViewModel里面。如果你能為Viewmodel寫單元測試而沒有創建任何UI對象,你也就能完全地剝離ViewModel因為它不依賴于指定的可見元素。?
最后,對于視覺設計的開發人員,使用MVVM使得創建平滑的設計者/開發者工作流變得更加簡單。由于View只是ViewModel的任意消費者,很容易剝離View而換一個新的View。這個簡單的步驟允許我們進行快速的原型設計以及設計人員進行UI的評估。
開發團隊可以將注意力集中于健壯的ViewModel類的創建。設計人員可以專注于創建用戶友好的Views。連接這兩個團隊的輸出除了確保Xaml里面的綁定存在可能設計更多。
演示程序
此刻,我已經查閱了MVVM的歷史和理論操作。我調查了為什么它在WPF開發者中這么流行。現在,我們看看這個模式是如何運作的。這篇文章的演示程序用多種方式來實現MVVM。它提供了豐富的實例用以將概念融入實際的背景中。我在Visual?Studio?2008?SP1,.NET框架3.5?SP1中創建這個演示程序。單元測試運行在Visual?Studio單元測試系統。
程序可能包含多個“工作區”,用戶可以點擊左邊導航區域將它們打開。所有的“工作區”在主要內容區域的TabControl?中展示。用戶可以點擊Tab項的關閉按鈕來關閉一個工作區。應用程序有兩個可用的工作區:"All?Customers"?和"New?Customer"??。
運行程序并且打開一些工作區之后,界面如下:
圖?1?工作區
一次只能打開一個"All?Customers"?工作區,但是可以同時打開多個"New?Customer"?工作區。當用戶想創建一個新客戶的時候,他必須填寫圖2?的表格數據。
圖2?用戶填寫數據表格
當用戶填寫有效的數據,并且點擊Save按鈕的后,新的用戶名字出現在標簽項中并且這個用戶被添加在?All?customers中。此程序不支持編輯和刪除已存在的用戶,但是此功能以及其它很多相似的功能在建立好應用程序框架之后是很容易實現的。現在你已經很好的理解了這個示例程序所作的,讓我們研究它是如何設計與實現的吧!
Relaying?Command?邏輯
除了類構造函數里面的InitializeComponent?生成的標準模板代碼,程序的每一個View都有一個codebehind?文件。實際上,你可以從工程中移除掉view的codebehind?文件,應用程序將仍然能正確地編譯和運行。?盡管View中沒有事件處理方法,當用戶點擊按鈕的時候,程序仍然能夠響應用戶的請求。這是因為綁定建立在UI控件,例如超鏈接,按鈕和菜單項控件上的Command屬性上。綁定使得用戶在控件上點擊的時候,ViewModel?的ICommand對象被執行。你可以把command對象作為一個適配器,它使得從XAML中聲明的視圖中調用ViewModel的功能變得簡單。當ViewModel展示了ICommand的一個實例時,Command對象通過ViewModel去完成它的工作。一種可行的實現模式是在ViewModel類中創建一個私有的嵌套類,一遍Command可以使用ViewModel類中的私有成員。并且不會污染到命名空間。這個嵌套類實現了ICommand接口,并且將ViewModel的一個應用注入到它的一個構造函數中。然而為每一個實現了ICommand接口的Command創建嵌套類可能使得ViewModel類變得膨脹,越多的代碼意味著越多的Bug存在的可能性。
在本程序中,RelayCommand?類解決了這個問題。RelayCommand?允許你通過代理注入Command的邏輯到構造函數中。這種方法允許在ViewModel類中以簡潔的,簡明的命令實現。RelayCommand?是Microsoft?Composite?Application?Library種中DelegateCommand?的簡化。圖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
}復制代碼
圖?3
ICommand?接口實現的一部分——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?接口,它們經常需要一個用戶友好的顯示名稱并且它們需要能夠關閉實例中的工作區。這個問題使得很自然的需要去創建一兩個ViewModel的基類,以便所有的新的ViewModel類能夠繼承自實現了這些通用功能的基類。ViewModel?類的繼承層次如下圖4:
圖4?繼承層次
?
?
ViewModelBase類
ViewModelBase?是繼承關系中的根類,這就是為什么它實現通用的INotifyPropertyChanged接口并且有一個DisplayName屬性。INotifyPropertyChanged?接口包含了一個叫PropertyChanged的事件。無論什么時候ViewModel?對象的一個屬性有一個新值,它會激活PropertyChanged?事件通知數據綁定系統這個新值。根據通知,數據綁定系統查詢到這個屬性,一些UI元素上的屬性也會接受到這個新值。為了讓WPF知道ViewModel對象的哪一個屬性發生改變,PropertyChangedEventArgs類公開一個String類型的PropertyName屬性。你必須小心的傳遞屬性名到事件參數中。否則,WPF將停止為一個新值查詢錯誤的屬性。
ViewModelBase?的一個有趣的地方是它提供了驗證在ViewModel對象中實際存在的一個屬性名。這在重構中是非常有用的,因為通過VS2008?重構屬性改變一個屬性的名字將不會修改源代碼中包含了屬性名的字符串。在事件參數中用錯誤的屬性名觸發PropertyChanged?事件將會導致難以捕獲的Bug,因此這個小的特性將節省很多時間。在圖?5?中ViewModelBase?的代碼中增加了此支持。
// In ViewModelBase.cspublic 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);
}
} 復制代碼
圖?5
CommandViewModel類
ViewModelBase?最簡單的子類是CommandViewModel。它展示了一個叫Command?的ICommand類型的屬性。MainWindowViewModel?通過它的Commands?屬性展示了這些對象集合。主界面的左邊的區域展示了MainWindowViewModel中每個CommandViewModel?的鏈接。例如"view?all?customers"?和"Create?new?customer"。當用戶點擊一個鏈接,就會執行他們其中的一個命令,一個工作區就會在commands中打開。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文件中存在一個名為"CommandsTemplate"的數據模板,MainWindow用模板去映射前面提到的CommandViewModels?集合。?這個模板簡單的將每一個CommandViewModel?對象作為ItemsControl?的一個鏈接。每一個HyperLink的Command屬性綁定到CommandViewModel的Command屬性。XAML?在圖6中顯示:
<!-- In MainWindowResources.xaml--><!--
This template explains how to render the list of commands on
the left sidein 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>復制代碼
圖?6
MainWindowViewModel?類
正如在前面的類圖中看到了,WorkspaceViewModel?類從ViewModelBase?繼承并且添加了關閉的功能。通過“Close”,在運行時可以將一些用戶界面從工作區移除。有三個類繼承自WorkspaceViewModel:MainWindowViewModel,?AllCustomersViewModel,和CustomerViewModel。MainWindowViewModel類的關閉請求是被App類來處理。它創建了MainWindow?以及它的ViewModel。如圖7:
// In App.xaml.csprotected 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();
}復制代碼
圖7
MainWindow?包含了一個菜單項,它的Command?屬性綁定到MainWindowViewModel的CloseCommand?屬性。當用戶點擊菜單項時,App類調用window's?Close?來響應,如下:
<!-- 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?項有一個關閉按鈕,它的Command屬性綁定到相應WorkspaceViewModel?實例的CloseCommand?。一個配置每個Tab項的模板的縮減版本顯示在下面的代碼中。這些代碼可以在MainWindowResources.xaml中找到,這些模板解釋了如何將Tab?項和關閉按鈕映射。
<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項的關閉按鈕,此工作區ViewModel的CloseCommand?命令就會執行。引起RequestClose?事件被觸發。MainWindowViewModel?監視工作區的RequestClose?事件,并且根據請求將工作區移除。由于MainWindow的TabControl??的ItemsSource屬性綁定到了WorkspaceViewModels的observable?集合,從集合中移除一項會導致TabControl移除相應的工作區。MainWindowViewModel?的邏輯如?圖8:
// In MainWindowViewModel.csObservableCollection<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 workspacein e.NewItems)
workspace.RequestClose+= this.OnWorkspaceRequestClose;
if (e.OldItems!= null && e.OldItems.Count!= 0)
foreach (WorkspaceViewModel workspacein e.OldItems)
workspace.RequestClose-= this.OnWorkspaceRequestClose;
}
void OnWorkspaceRequestClose(object sender, EventArgs e)
{
this.Workspaces.Remove(senderas WorkspaceViewModel);
} 復制代碼
圖8
在UnitTests?工程中,MainWindowViewModelTests.cs包含了方法驗證此功能是否正常工作。很容易為ViewModel?創建單元測試是MVVM的一個很大的亮點。因為它允許無需寫UI代碼的情況下進行一些簡單的功能測試。測試方法如?圖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.");
} 復制代碼
圖9
將View運用到ViewModel
MainWindowViewModel?間接的從主窗體的TabControl控件添加與移除MainWindowViewModel?對象。通過依賴數據綁定,TabItem的Content?屬性接受到派生自ViewModelBase的類對象去顯示。ViewModelBase?不是UI元素,因此它沒有內在的支持去顯示自己。在WPF中默認展示一個不可視對象是通過TextBlock中調用此對象的ToString方法得到的字符串來顯示。這明顯不是你所想要的,除非用戶強烈的希望看到ViewModel?的類型名稱。
你可以簡單的通過類型化的DataTemplate告訴WPF如何展示一個ViewModel?對象。類型化的DataTemplate沒有一個x:Key值分配給它,但是它有DataType屬性去設置為一個Type類的實例。如果WPF試著展示一個ViewModel?對象,它會確認資源系統是否有一個類型化的DataTemplate,它的DataType?與ViewModel?對象的類型是一樣的(或者是它的基類)。如果找到了,它用那個模板在Tab?item的Content屬性里去呈現引用的ViewModel對象。MainWindowResources.xaml文件有一個ResourceDictionary。那個字典被添加到主窗體的資源體系中,這意味著它包含的資源在窗口的資源范圍里。當一個Tab項的Content被設置為ViewModel?的對象時,字典中的一個DataTemplate提供一個View去顯示它。如圖?10:
<!--This resource dictionaryis 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 AllCustomersViewModelclass shownin the main window.
-->
<DataTemplate DataType="{x:Type vm:AllCustomersViewModel}">
<vw:AllCustomersView/>
</DataTemplate>
<!--
This template applies a CustomerView to an instance
of the CustomerViewModelclass shownin the main window.
-->
<DataTemplate DataType="{x:Type vm:CustomerViewModel}">
<vw:CustomerView/>
</DataTemplate>
<!-- Other resources omittedfor clarity...-->
</ResourceDictionary> 復制代碼
圖?10
你不需要寫任何代碼來決定哪一個View來顯示ViewModel?對象。WPF資源系統為你做好了一切,使你集中精力于更加重要的東西。在更多復雜的場景中,可能需要編程去選擇View,但是大多數情況下那是不需要的。
數據模型和資源庫
你已經看到ViewModel對象如何被應用程序框架加載,顯示和關閉。?現在基本的框架已經搭建好了,你可以專注與程序的實現細節。在深入的了解程序的兩個工作區"All?Customers"?and?"New?Customer"之前,讓我們查看一個數據模型和數據訪問類。那些類的設計幾乎與MVVM模式無關。因為你可以創建一個ViewModel類去適應任何WPF支持的數據對象。示例中唯一的Model類是Customer。這個類有一些屬性代表了一個公司員工的信息,例如他們的姓名,Email地址。它通過實現標準的IDataErrorInfo?接口實現了驗證消息,這在WPF流行之前已經存在好幾年了。Customer?類沒有任何東西表明它被用在MVVM架構或是WPF程序中。這種類能很容易源自業務庫。
數據必須來源并存在于某個地方,在這個程序中CustomerRepository?的一個實例加載并且存儲所有的Customer?對象。它從XML?文件中加載數據,但是外部的數據源是無關緊要的。數據可以源自數據庫、Web?Service、命名管道、磁盤文件、甚至信鴿,這完全都無所謂。只要你有一個包含了數據的.Net對象,不管它來自哪里,MVVM模式都可以得到數據并顯示在屏幕上。
CustomerRepository?類提供了一些方法讓你訪問所有可用的Customer?對象,添加新的Customer到集合中,同時判斷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");
}
}復制代碼
當用戶創建一個新的Customer并且點擊CustomerView的Save按鈕時,與View關聯的CustomerViewModel?將添加一個新的Customer?到Customer集合。這導致了CustomerAdded?事件被激活,這個事件通知AllCustomersViewModel應該將新的Customer添加到AllCustomers?集合。在某種意義上,CustomerRepository類在各種處理Customer對象的ViewModel類之間起到一個同步機制的作用。可能有人會把這當做Mediator?設計模式的使用。在下面的部分我將細述它是如何工作的,但是現在看一下圖11?對各部分如何組裝的有一個整體的了解。
圖?11
新Customer數據輸入表格
當用戶點擊"Create?new?customer"鏈接,MainWindowViewModel?會在它的工作區列表中添加一個新的CustomerViewModel,同時一個新的CustomerView?控件去顯示它。當用戶將有效的類型值輸入到輸入框中,Save?按鈕變為可用狀態以便用戶可以保存新的Customer?的信息。沒有什么與眾不同的東西,僅僅是一個包含輸入驗證和Sava按鈕的數據表格。
Customer?類已經通過實現IDataErrorInfo?接口內置了輸入驗證的支持。此驗證確保了Customer有一個FirstName,正確格式的e-mail?地址,并且如果用戶是一個人的話,還有一個LastName。如果Customer?的IsCompany返回?true,LastName?就不能有值(因為一個公司沒有LastName)。這個驗證的邏輯從客戶對象的角度來看可能是有意義的,但是它不滿足界面的需求。?用戶界面需要用戶去選擇一個新的Customer是人還是公司。客戶類型選擇器初始值為"Not?Specified"。那么如果一個Customer的IsCompany屬性僅僅允許true或者false的值,界面是怎樣去告訴用戶客戶類型是"Not?Specified"?的呢??
假設你已經完成了整個軟件系統,你應該將IsCompany?屬性改為Nullable<bool>類型?類型,這可以允許"unselected"值。?但是,真實的世界沒有這么簡單,假設你不能改變Customer?類因為它來自你們公司的另一個團隊的類庫。如果存在數據庫的設計而不能保存"unselected"值將會怎樣?如果其他應用程序已經使用Customer類而且依賴于它是一個正常的布爾值將會怎樣?又一次地,ViewModel來幫忙。圖12?的測試方法展示了此功能如何在CustomerViewModel工作的。CustomerViewModel?暴露了一個CustomerTypeOptions?的屬性以便Customer?的類型可以顯示為第三種字符串。它也暴露了一個CustomerType屬性來存儲選擇的字符串。當CostomerType被賦值時,它將字符串值轉換為一個布爾值并提供給為潛在的Customer對象的IsCompany屬性。圖13顯示了這兩個屬性。
// 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= (targetas IDataErrorInfo)["CustomerType"];
Assert.IsFalse(String.IsNullOrEmpty(error),"Error message should
be returned");
} 復制代碼
圖12
// In CustomerViewModel.cspublic 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");
}
} 復制代碼
圖13
? CustomerView控件包含一個ComboBox來綁定到這些屬性,如下:
<ComboBoxItemsSource="{Binding CustomerTypeOptions}"
SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}" /> 復制代碼
當ComboBox?的選擇改變的時候,程序會查詢數據源的IDataErrorInfo接口以檢查新的值是否有效。此過程發生的原因是SelectedItem屬性綁定將ValidateOnDataErrors設置為true。由于數據源是CustomerViewModel?對象,綁定系統在CustomerViewModel的?CutomerType屬性上查詢驗證錯誤。大多數情況下,CustomerViewModel?會代理所有Customer?對象包含的錯誤驗證請求。然而,由于Customer?的IsCompany屬性沒有為選擇狀態,CustomerViewModel?類必須處理在ComboBox?控件上新的選擇項的驗證。代碼如?圖14:
// In CustomerViewModel.csstring 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= (_customeras 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";
} 復制代碼
圖14
代碼的關鍵是CustomerViewModel對IDataErrorInfo?接口的實現ViewModel指定屬性驗證的請求并且將其它的請求委托到Customer?對象。這允許你使用Model?類中的驗證邏輯,而且附加的屬性驗證僅僅對于ViewModel類有意義。
通過SaveCommand?屬性使得CustomerViewModel?可以被保存。此命令運用RelayCommand?類去檢查CustomerViewModel?是否能保存自己并且當被告知保存它的狀態時該做什么。在此程序中保存一個新的Customer意味著把它添加到CustomerRepository。決定一個新的客戶是否能被保存需要兩方面方的同意:Customer?對象必須被詢問是否通過了驗證,并且CustomerViewModel?必須確定它是否通過驗證。這兩部分的決定是必要的,因為ViewModel指定的屬性已經在之前被檢查過。CustomerViewModel?的保存邏輯如?圖15:
// In CustomerViewModel.cspublic 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;
}
} 復制代碼
圖15
在這里,使用ViewModel的使用使得下面這些事情變得更加簡單:創建一個現實Customer?對象的View以及允許Bool屬性為"unselected"?狀態。它也使得告知Customer保存它的狀態變得簡單。如果將Customer?直接綁定到View,View可能需要許多代碼以使得程序正常工作。在一個設計良好的MVVM?架構中,大部分的Views?的后置代碼應該是空的,或者之多包含一些控制控件或資源的代碼。有時候在View的后置代碼中寫一些代碼與ViewModel?交互是必須得。比如捕獲一個事件或調用一個方法,這個方法直接從ViewModel對象自己調用時非常困難。
所有Customer視圖
演示程序同樣包含了一個在ListView中顯示所有Customer的工作區,此類表中的Customer是通過它是工公司或是個人來分類的。用戶可以同時選擇一個或者多個Customer并且在底部右下角查看總銷售額。
用戶界面是AllCustomersView控件,它對應一個AllCustomersViewModel對象。每一個列表項代表一個AllCustomersViewModel對象暴露的AllCustomers集合中的一個CustomerViewModel對象。在前面的章節中,你看到了一個CustomerViewModel怎樣對應一個數據輸入表格,現在同樣的CustomerViewModel對象被映射作為列表中的一項。CustomerViewModel類并不知道什么樣的可見的元素來顯示它,這就是為什么它可以重用的原因。AllCustomersView?在ListView上創建了分組。這是通過綁定ListView的ItemsSource到CollectionViewSource來完成的。如圖16:
<!-- 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> 復制代碼
圖16
ListView'?的?ItemContainerStyle屬性確定了ListViewItem?和?CustomerViewModel?對象之間的聯系,此屬性的Style?被應用到每個ListViewItem,它使得ListViewItem?中的屬性綁定到CustomerViewModel對象。一個重要的綁定是在列表項的IsSelected屬性和CustomerViewModel的IsSelected屬性之間創建鏈接,如下:
<Style x:Key="CustomerItemStyle" TargetType="{x:Type ListViewItem}"><!-- Stretch the content of each cell so that we can right-align textin the Total Sales column.-->
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<!--
Bind the IsSelected property of a ListViewItem to the
IsSelected property of a CustomerViewModelobject.
-->
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
</Style> 復制代碼
當CustomerViewModel?被選擇或是反選的時候,會導致所有選擇的Customer的總銷售額改變。AllCustomersViewModel?負責維護這個值,以便在列表之下能顯示正確的數值。圖17?顯示了AllCustomersViewModel怎樣監視每個客戶被選擇或反選,并通知view更新顯示數值。
// In AllCustomersViewModel.cspublic 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.
(senderas 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?并且將值轉換為貨幣格式。通過返回一個字符串代替從TotalSelectedSales屬性得到的浮點數,ViewModel對象可以應用貨幣格式。在.NET框架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對應用程序開發者提供了許多,學習利用這個需要心態上的轉變。?MVVM設計模式對于設計和實現應用程序是簡單的并且有指導意義。它使得你可以創建數據、行為、展現的分離,使得控制軟件開發的混亂更加簡單。
點擊,代碼下載。
轉載于:https://www.cnblogs.com/jeriffe/articles/2287885.html
總結
以上是生活随笔為你收集整理的[译]WPF 应用程序和MVVM设计模式 ——Josh Smith的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: struts2+spring3.2简单d
- 下一篇: asp.net ajax控件工具集 Au