轉載自:http://msdn.microsoft.com/zh-cn/magazine/dd419663.aspx#MtViewDropDownText
模式 :WPF 應用程序使用程序的模型視圖 ViewModel 設計模式
Josh Smith
本文討論:
- 模式和 WPF
- MVP 模式
- 爲何 MVVM 最好爲 WPF
- 構建與 MVVM 應用程序
|
本文涉及如下技術: WPF,數據綁定 |
![](http://static.javashuo.com/static/loading.gif)
內容
專業的軟件應用程序的 開發用戶界面 不容易。
它能夠是數據、 交互設計、 可視化設計、 鏈接,多線程處理、 安全性、 國際化、 驗證、 單元測試和的 Voodoo 的觸摸一個渴融合。
考慮用戶界面公開基礎系統的和必須知足其用戶的不可預測的從句要求,它能夠是最易失方面不少應用程序。
還有,可幫助 tame 此不實用的 beast 的常見設計模式,但正確分隔並解決問題的多種很難。
在更復雜的模式是,越將快捷方式用於之後的破壞全部之前的努力執行的操做權限的方式。
不老是在設計模式,出現錯誤。
有時咱們使用須要編寫大量代碼,由於在使用的 UI 平臺不出借自己很好地簡單模式的複雜的設計模式。
須要將是一個平臺,更易於構建使用簡單、 time-tested、 開發人員批准的設計模式的 UI 它。
幸運的是,Windows Presentation Foundation (WPF) 提供了徹底的。
世界上繼續增長的速度採用 WPF 在軟件,WPF 社區已開發模式和實踐本身生態的系統。
此文章中, 我將討論一些用於設計和實現客戶端應用程序與 WPF 這些最佳方法。
利用 WPF 結合模型-視圖-ViewModel (MVVM) 設計模式) 的某些核心功能我將介紹的示例程序演示瞭如何簡單也能夠是構建 WPF 應用程序"正確方式"。
本文末尾它將會清除數據模板、 命令、 數據綁定,在資源系統和 MVVM 模式全部結合方式來建立一個簡單、 可測試、 功能強大的框架,的任何 WPF 應用程序能夠 thrive。
本文演示程序能夠做爲一個做爲其核心體系結構使用 MVVM 實際 WPF 應用程序模板。
單元測試演示解決方案中的顯示一組 ViewModel 類中存在的該功能時,測試應用程序的用戶界面的功能是多麼容易。
深刻詳細信息以前, 一下爲何應首先使用像 MVVM 模式。
訂單與混亂
是沒必要要的沒法在簡單"Hello,World !"程序中使用設計模式。
任何 competent 開發人員能夠了解幾行代碼一眼。
可是,隨着在程序中的功能的數的增長的代碼和移動部件的行數增長相應。
最終,系統和它所包含的重複問題的複雜性鼓勵開發人員能夠組織方式這樣作還會更便於他們代碼全世界、 討論、 擴展,並解決問題。
咱們經過將已知的名稱應用到在源代碼中的特定實體下降複雜系統的認知的混亂。
咱們肯定名稱以經過在系統中考慮其職能角色應用於一段代碼。
開發人員常常故意構造一個設計模式相對於讓咱們能夠看到 organically 文本模式根據其代碼。
是什麼不對的方法,可是本文中, 我檢查顯式使用 MVVM 爲 WPF 應用程序的體系結構的好處。
某些類別的名稱包括從 MVVM 模式如結尾"ViewModel,若是類是視圖的抽象的已知條件。
此方法有助於避免認知前面提到的混亂。
相反,您能夠使人高興的是存在是大多數專業軟件開發項目中的事件的天然狀態的控制混亂的狀態 !
模型視圖 ViewModel 的演變
ever 自人建立軟件用戶界面,已爲了使更容易的常見設計模式。
是例如 Model-視圖-演示者 (MVP) 模式已欣賞各類用戶界面編程平臺上的普及。
MVP 是模型-視圖-控制器模式已爲數十年的變體。
若是還不 MVP 模式以前用如下是簡化的說明。
在屏幕上看到爲視圖、 顯示的數據是模型,和演示者一塊兒掛鉤兩個。
視圖依賴於要填充模型數據,請對用戶輸入作出反應,提供輸入的驗證 (可能經過委派到模型) 和其餘此類任務的演示者。
若是您但願瞭解有關模型查看演示者,我建議您閱讀 Jean-Paul Boodhoo
2006 年 8 月設計模式列 .
在 2004,Martin Fowler 發佈有關命名模式的文章
演示文稿模型
(PM)。
分開的行爲和狀態視圖,PM 模式與相似 MVP。
值得關注的 PM 模式部分是視圖的抽象建立,稱爲演示文稿模型。
一個的視圖將,成爲只是演示文稿模型的呈現。
在 Fowler 的解釋他顯示演示文稿模型頻繁地更新其視圖,以便兩個保持與彼此保持同步。
該同步邏輯存在演示文稿模型類中的代碼。
2005 中, 當前的 WPF 和 Silverlight 架構師,在 Microsoft,一個的 John Gossman unveiled 在
模型-視圖-ViewModel (MVVM) 模式
在他的博客。
MVVM 是與 Fowler 的演示文稿模型,這兩種模式功能一個視圖包含視圖的狀態和行爲的抽象。
fowler 而 Gossman 做爲標準化能夠利用 WPF 來簡化用戶界面建立的核心功能引入 MVVM,做爲一種建立 UI 的獨立於平臺的抽象一個的視圖引入演示文稿模型。
此種意義上講,我認爲 MVVM 爲更多常規 PM 圖案,tailor-made WPF 和 Silverlight 平臺的一個特例。
Glenn 塊的極好文章"中
構建複合應用程序與 WPF 的 prism: 模式
"2008 年 9 月刊中, 他說明爲 WPF Microsoft 複合應用程序指導。
ViewModel 從未使用過的術語。
相反,術語演示文稿模型用於描述視圖的抽象。
在本文,可是,我將引用 MVVM 和視圖的抽象,做爲一個 ViewModel 模式。
我發現此術語是 WPF 和 Silverlight 社區中的更多 prevelant。
與 MVP 中的對演示者不一樣一個 ViewModel 不須要對視圖的引用。
視圖綁定到一個 ViewModel 這反過來,公開模型對象和其餘狀態特定於視圖中包含的數據的屬性中。
視圖和 ViewModel 之間綁定是簡單構造因爲一個 ViewModel 對象被設置爲視圖的 DataContext。
若是屬性值在 ViewModel 更改,這些新值自動傳播到經過數據綁定的視圖。
當用戶單擊一個按鈕在視圖時, 在 ViewModel 的命令將執行執行所請求的操做。
ViewModel,永遠不會在視圖,執行模型數據所作的全部修改。
在的視圖類有模型類存在,不知道該視圖的 ViewModel 和模型時不知道。
實際上,模型是徹底 oblivious 事實存在 ViewModel 和視圖。
這是最鬆散耦合設計,多種方式支付股利,正如您很快就將看到的。
爲何 WPF 開發人員喜歡 MVVM
一旦開發人員成爲熟悉 WPF 和 MVVM,很難區分這二者。
MVVM 是 WPF 開發人員的語言 franca,由於它是適合在 WPF 平臺 WPF 爲了方便地構建應用程序使用 MVVM 模式 (在其餘)。
實際上,Microsoft 使用 MVVM 內部開發 WPF 應用程序,Microsoft Expression Blend,如,核心 WPF 平臺時正在建設中。
WPF,如外觀不控制模型和數據模板的許多方面使用顯示的狀態和行爲的 MVVM 提高強的分離。
在單個的最重要方面,WPF 使 MVVM 好模式使用的是數據綁定基礎結構。
由一個 ViewModel 的視圖的綁定屬性,您得到兩者之間的鬆散耦合,並徹底刪除須要一個 ViewModel 直接更新視圖中編寫代碼。
數據綁定系統還支持提供了標準化的方式傳輸到視圖的驗證錯誤的輸入的驗證。
兩個其餘功能作這種模式所以可用的是 WPF 的數據模板和資源系統。
數據模板應用於在用戶界面中顯示的 ViewModel 對象的視圖。
能夠聲明在 XAML 中的模板,並讓資源系統自動查找併爲您應用這些模板,在運行時。
您能夠了解詳細有關綁定和我 7 月 2008 文章中的數據模板"
數據和 WPF: 使用數據綁定和 WPF 中自定義數據顯示
."
若是未在 WPF 中的命令的支持中,MVVM 模式是得強大。
本文,我將介紹如何在 ViewModel 能夠公開一個的視圖的命令從而使視圖以使用它的功能。
若是您不熟悉控制,我建議您閱讀 Brian Noyes 全面文章"
高級 WPF: 瞭解路由事件和 WPF 中的命令
"摘自 2008 年 9 月刊。
除了在 WPF (和 Silverlight 2) 功能,使一個天然的方式構建應用程序的 MVVM,模式也是受歡迎,由於 ViewModel 類是易於單元測試。
應用程序的交互邏輯居住在一組 ViewModel 類中時, 能夠輕鬆地編寫測試它的代碼。
在一個的意義上的視圖和單元測試兩個不一樣類型類型均 ViewModel 使用者。
爲應用程序的 ViewModels 有一套測試的提供忙 / 快速回歸測試,有助於下降維護應用程序隨着時間的成本。
除了提高自動的迴歸測試的建立,ViewModel 類的 testability 能夠幫助正確設計用戶界面,能夠很容易地外觀。
在設計應用程序時您一般能夠決定是否內容應在視圖和要編寫單元測試能夠佔用該 ViewModel 經過 imagining ViewModel。
若是您能夠在 ViewModel 的編寫單元測試,而不建立任何 UI 對象,由於它不有特定的可視元素上的任何依賴項還徹底能夠外觀,ViewModel。
最後的開發人員使用可視化設計器,使用 MVVM 使得更易於建立平滑的設計器 / Developer 工做流。
因爲視圖是隻需一個任意消費者一個 ViewModel,它很容易就翻錄出的一個視圖和要呈現一個 ViewModel 新視圖中的下拉。
此簡單步驟容許快速原型和用戶界面由設計器的計算。
開發團隊能夠專一於建立功能強大的 ViewModel 類和設計團隊能夠集中精力進行用戶友好的視圖。
鏈接兩個團隊的輸出可能涉及小超過確保正確綁定存在視圖的 XAML 文件中。
演示應用程序
到目前爲止我已審閱 MVVM 的歷史記錄和操做的理論。
我還檢查爲什麼如此流行間 WPF 開發人員。
如今,它是以彙總您袖子,並查看模式中的時間。
本文附帶演示應用程序使用 MVVM 各類方式。
它提供示例,以幫助置於一個有意義的上下文的概念 fertile 的源。
我是在 Visual Studio SP 2008 1,與 Microsoft.NET Framework 3.5 SP 1 中建立的演示應用程序。
Visual Studio 單元測試系統中,運行單元測試。
應用程序能夠包含任意數量的工做"區,"用戶能夠打開每一個經過單擊在左側導航區域中的命令連接。
全部工做區主內容區域 Live 一個 TabControl 中。
經過單擊該工做區選項卡項目上的關閉按鈕,用戶可關閉工做區。
該應用程序有兩個可用的工做區:"All Customers"和"新客戶"。 運行該應用程序並打開某些工做區後, 在用戶界面相似 圖 1 。
圖 1
工做區
"All Customers"工做區的只有一個實例能夠一次,但任意數量的"新客戶"打開工做區能夠打開一次。
若是用戶決定建立新客戶,用戶必須填寫 圖 2 中的數據項表單。
圖 2
新客戶數據輸入表單
填寫數據輸入表單具備有效的值,並單擊保存按鈕中,新客戶的名稱將顯示在選項卡後項目和客戶添加到全部客戶的列表。
應用程序沒有刪除或編輯現有客戶的支持,但該的功能和許多相似,其餘特性很容易經過構建實現現有應用程序體系結構的頂部。
如今,有一個高級別的瞭解演示應用程序的用途,讓咱們研究如何設計並實現。
中繼命令邏輯
在應用程序中的每一個視圖具備除外的類的構造函數中調用 InitializeComponent 在標準的樣板代碼爲空的源代碼文件。
事實上,能夠從項目中刪除視圖的代碼隱藏文件和應用程序仍將編譯和正常運行。
儘管視圖中的事件處理方法缺少當用戶單擊按鈕上, 時應用程序的反應並知足用戶的請求。
這是因已創建的綁定適用於超連接、 按鈕和 MenuItem 控件顯示在用戶界面中的 Command 屬性。
這些綁定確保當用戶單擊控件上,ICommand 對象公開,ViewModel 將執行。
能夠將 Command 對象視爲更易於使用從視圖在 XAML 中聲明的 ViewModel 的功能的適配器。
當一個 ViewModel 公開實例屬性的類型 Icommand 時,Command 對象將一般使用 ViewModel 對象來獲取完成其工做。
一個可能的實現模式是建立在 ViewModel 類中的私有嵌套的類,以便命令有權訪問其包含 ViewModel 的私有成員並不會 pollute 命名空間。
該嵌套的類實現該 ICommand 接口,並對包含 ViewModel 對象的引用注入其構造函數。
可是,建立 ICommand 實現對於由一個 ViewModel 提供每一個命令的嵌套的類能夠 bloat ViewModel 類的大小。
更多代碼意味着更高版本的可能的錯誤。
演示應用程序中 RelayCommand 類解決了這個問題。
RelayCommand 容許您將經過傳遞給其構造函數的委託的命令的邏輯。
此方法容許簡潔、 簡潔命令實現 ViewModel 類中。
RelayCommand 是中找到的 DelegateCommand 的簡化變體,
Microsoft 複合應用程序庫 .
relaycommand 類如 圖 3 所示。
![](http://static.javashuo.com/static/loading.gif)
圖 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
}
CanExecuteChanged 事件屬於該 ICommand 接口實現具備一些有趣的功能。
該委託 CommandManager.RequerySuggested 事件事件訂閱。
這能夠確保 WPF 控制基礎結構要求全部 RelayCommand 對象若是它們能夠執行它要求內置命令時。
如下代碼從 CustomerViewModel 類我將探討詳細更高版本,說明如何配置一個 RelayCommand 與 lambda 表達式:
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
繼承層次結構
全部您 ViewModels 需從基類不包括是必要條件。
若是您但願經過一塊兒,撰寫許多較小的類,而不是使用繼承,得到您的類中的功能的不是問題。
像其餘任何設計模式 MVVM 是一個指南,不規則組。
ViewModelBase 類
ViewModelBase 是根類別在層次結構是緣由它實現經常使用的 INotifyPropertyChanged 接口,並具備 DisplayName 屬性中。
INotifyPropertyChanged 接口包含名爲 PropertyChanged 的事件。
只要 ViewModel 對象上的一個屬性具備新值,它能夠引起通知新值的 WPF 綁定系統 PropertyChanged 事件。
接收的通知,時綁定系統查詢屬性,並在某些用戶界面元素將綁定的屬性接收新值。
爲了瞭解 ViewModel 對象的屬性已更改的 WPF PropertyChangedEventArgs 類將公開 String 類型的一個 PropertyName 屬性。
您必須注意,將正確的屬性名稱傳遞到該事件參數 ; 不然,WPF 將獲得查詢新值不正確的屬性。
ViewModelBase 的一個有趣方面是它可以驗證一個屬性具備給定名稱確實存在 ViewModel 對象上。
這是很是有用重構時, 由於更改經過 Visual Studio 2008 重構功能的屬性的名稱不會更新在源代碼中會包含該屬性名稱的字符串也 (不該它)。
事件參數會致使很難跟蹤,所以此不多的功能能夠極大的 timesaver 的細微錯誤,請引起 PropertyChanged 事件與不正確的屬性名稱。
添加此有用的支持的 ViewModelBase 了代碼如 圖 5 所示。
![](http://static.javashuo.com/static/loading.gif)
圖 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 子類是 CommandViewModel。
它公開一個名爲類型 Icommand 的命令屬性。
MainWindowViewModel 公開這些對象經過其命令屬性的集合。
主窗口的左側導航區域顯示一個連接的每一個 CommandViewModel 公開 MainWindowViewmodel,如"查看全部客戶"和"建立新客戶"。 當用戶單擊連接時,從而執行某個這些命令在工做區打開 TabControl 在主窗口中。
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 一個 datatemplate。
MainWindow 使用該模板呈現 CommandViewModels 前面提到的集合。
該模板只呈現爲一個 ItemsControl 中的連接的每一個 CommandViewModel 對象。
每一個超連接的 Command 屬性綁定到一個 commandViewModel 的 Command 屬性。
該 XAML 如 圖 6 所示。
![](http://static.javashuo.com/static/loading.gif)
圖 6 呈現命令的列表
<!-- 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>
MainWindowViewModel 類
爲在類關係圖中之前看到,WorkspaceViewModel 類派生自 ViewModelBase,並添加功能關閉。
經過"關閉"我意味着內容刪除工做區用戶界面在運行時。
三個類派生 WorkspaceViewModel: MainWindowViewModel,AllCustomersViewModel,和 CustomerViewModel。
MainWindowViewModel 的請求以關閉由建立該 MainWindow 和其 ViewModel,如 圖 7 所示在應用程序類處理。
![](http://static.javashuo.com/static/loading.gif)
圖 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 包含其命令屬性綁定到 MainWindowViewModel 的 CloseCommand 屬性的菜單項。
當用戶單擊該菜單項時,應用程序類響應,經過調用窗口的 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 對象的一個可見的集合。
在主窗口中包含的 ItemsSource 屬性綁定到該集合在 TabControl。
每一個選項卡項目都有的 Command 屬性綁定到其相應的 WorkspaceViewModel 實例的 CloseCommand 關閉按鈕。
配置選項卡的每一項的模板的 abridged 的版本所示的代碼。
代碼位於 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>
當用戶單擊 workspaceViewModel 的 CloseCommand 執行致使其 requestClose 事件觸發的選項卡項目中關閉按鈕。
MainWindowViewModel 監視其工做區的 RequestClose 事件,並從請求 Workspaces 集合中刪除工做區。
由於 mainwindow 的 TabControl 有綁定到 WorkspaceViewModels 的可見集合的 ItemsSource 屬性,從集合中刪除項目會致使從 TabControl 刪除相應的工做區。
圖 8 顯示從 mainWindowViewModel 的邏輯。
![](http://static.javashuo.com/static/loading.gif)
圖 8 從用戶界面中刪除工做區
// 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 模式一個大銷售點,由於它容許進行簡單測試的應用程序功能無需編寫代碼與用戶界面。
該測試方法如 圖 9 所示。
![](http://static.javashuo.com/static/loading.gif)
圖 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.");
}
將視圖應用於一個 ViewModel
MainWindowViewModel 間接添加並刪除與主窗口 tabcontrol workspaceViewModel 對象。
利用數據綁定,一個 TabItem 的 Content 屬性接收 ViewModelBase 派生對象,以顯示。
ViewModelBase 不是一個 UI 元素,所以它具備不用於呈現其自身的內在支持。
默認,在 WPF 中非 Visual 對象呈如今 TextBlock 中顯示其 ToString 方法調用的結果。
清楚地是否是須要除非您的用戶具備能夠看到咱們 ViewModel 類的類型名稱的刻錄要求 !
您能夠很容易地判斷 WPF 如何經過呈現一個 ViewModel 對象鍵入 DataTemplates。
類型化的 DataTemplate 沒有分配給它的 x: Key 值,但它不會將設置爲類型類的實例其數據類型屬性。
若是 WPF 嘗試呈現您 ViewModel 對象之一,它將檢查,請參閱若是資源系統具備類型化的 DataTemplate 做用域中的數據類型是相同 (或的基類) 您 ViewModel 對象的類型。
若是找到,它將使用該模板呈現引用的選項卡該項的內容屬性 ViewModel 對象。
MainWindowResources.xaml 文件具備一個 resourcedictionary。
該詞典添加到主窗口的資源層次這意味着它所包含的資源位於窗口的資源做用域。
當選項卡項的內容設置爲一個 ViewModel 對象時,此詞典中的類型化的 DataTemplate 提供視圖 (這就是用戶控件) 來呈現其,如 圖 10 所示。
![](http://static.javashuo.com/static/loading.gif)
提供視圖的圖 10
<!--
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>
您沒必要編寫任何代碼肯定該視圖以顯示一個 ViewModel 對象。
WPF 資源系統將會繁重的工做的全部爲您在您釋放到重點更重要的事情。
更復雜的狀況可能會以編程方式選擇視圖但在大多數狀況下沒必要要的。
在數據模型和存儲庫
您已經瞭解如何加載、 顯示,和關閉應用程序外殼程序 ViewModel 對象。
如今,常規管道就地,您能夠查看到應用程序的域更具體的實現細節。
深刻了解應用程序的兩個 Workspaces"All Customers"以前,"新客戶"讓咱們首先檢查數據模型和數據訪問類。
這些類的設計無關幾乎與在 MVVM 模式由於您能夠建立一個 ViewModel 類,以適應到內容爲 WPF 的友好的幾乎任何數據對象。
惟一的模型類演示程序中是客戶。
此類有少許的屬性表示公司如其名、 上次的用戶名和電子郵件地址的客戶的信息。
它經過實現年 WPF 點擊該街道以前存在的該標準 IDataErrorInfo 接口提供驗證信息。
客戶類中建議的 MVVM 體系結構中或甚至 WPF 應用程序正在使用它的包含執行任何操做。
類能夠輕鬆地來自舊業務庫。
數據必須來自並駐留在某處。
在此應用程序,CustomerRepository 類的實例加載,並存儲全部的 Customer 對象。
發生從一個 XML 文件加載客戶數據,但外部數據源的類型是不相關。
數據可能來自數據庫、 Web 服務、 命名的管道、 磁盤,或偶數運營商 pigeons 上的文件: 只是不重要。
只要不管它來自,在其中,具備.NET 對象的某些數據,MVVM 模式就能夠在屏幕上得到該數據。
CustomerRepository 類公開,容許您獲取全部可用的客戶對象的幾個方法添加新客戶到存儲庫,並檢查是否客戶是否已在存儲庫中。
因爲應用程序不容許用戶刪除客戶,存儲庫不容許刪除客戶。
當新的客戶將 CustomerAdded 事件激發輸入 CustomerRepository,經過 AddCustomer 方法。
清楚地,此應用程序數據模型是很是小與實際的業務應用程序要求什麼,但的不是重要。
是瞭解是如何 ViewModel 類建立使用客戶和 CustomerRepository。
請注意該 customerViewModel 是客戶對象周圍的包裝。
它提供了一個的客戶狀態和其餘由 customerview 控件的經過一組屬性的狀態。
CustomerViewModel 重複的客戶的狀態 ; 它只是公開它經過委派,以下:
public string FirstName
{
get { return _customer.FirstName; }
set
{
if (value == _customer.FirstName)
return;
_customer.FirstName = value;
base.OnPropertyChanged("FirstName");
}
}
當用戶建立新的客戶,並單擊中 CustomerView 控件的保存按鈕時,在 CustomerViewModel 與關聯的視圖將添加新的客戶對象在 customerrepository。
致使的 CustomerAdded 事件觸發,它容許知道它應到其 AllCustomers 集合中添加新的 CustomerViewModel 的 AllCustomersViewModel。
一種意義上講 Customerrepository 充當 Customer 對象所處理的各類 ViewModels 之間的同步機制。
多是一個可能將這視爲使用中介設計模式。
我會查看多個工做在即將進行的部分,但如今方式引用到 圖 11 中圖表方式的高級瞭解全部代碼段結合。
圖 11
客戶關係
新客戶數據輸入窗體
當用戶單擊"建立新客戶"連接時,MainWindowViewModel 將添加到其列表的工做區中, 新的 CustomerViewModel 並 CustomerView 控件顯示。
用戶輸入到輸入域的有效的值後,以便用戶能夠保持新的客戶信息保存按鈕就會進入啓用的狀態。
沒有在普通此處,只是常規數據輸入表單與輸入驗證和保存按鈕。
客戶該類具備支持,可經過其 IDataErrorInfo 接口實現的內置驗證。
將確保驗證 (客戶具備第一個名稱標準格式的電子郵件地址,且,客戶爲人員,姓氏。
若是客戶的 IsCompany 屬性返回 True,LastName 屬性不能在 (其目的所在的公司沒有某一姓名) 具備一個值。
此驗證邏輯可能意義從客戶對象的角度但它不能知足用戶界面的須要。
在用戶界面要求在用戶選擇新的客戶是我的或公司。
客戶類型選擇器最初具備值"(不指定)"。
如何能夠在用戶界面告訴用戶是否爲 True 或 False 值只容許客戶的將 IsCompany 屬性,客戶類型是未指定?
假定在您有徹底控制整個軟件系統,能夠更改 IsCompany 屬性類型可空 <bool>,這將容許爲"未選定"的值。
可是,實際不老是如此簡單。
假設您不能更改客戶類,由於它來自舊庫歸您的公司中的不一樣團隊。
若是有是不容易方式保留的"未選中"值因現有的數據庫架構?
若是其餘應用程序已使用客戶類,並依賴一個普通的 Boolean 值: 屬性?
再一次遇到一個 ViewModel 提供到該恢復。
圖 12 中的,測試方法顯示此功能工做方式 CustomerViewModel。
以便客戶類型選擇器有三個字符串顯示,CustomerViewModel 將公開一個 CustomerTypeOptions 屬性。
它還公開一個客戶類型屬性選擇器中存儲所選的字符串。
設置客戶類型時, 它將映射到基礎客戶對象的 IsCompany 屬性布爾值的字符串值。
圖 13 顯示了兩個屬性。
![](http://static.javashuo.com/static/loading.gif)
圖 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");
}
![](http://static.javashuo.com/static/loading.gif)
圖 13 客戶類型屬性
// 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}"
/>
該組合框中的選定的項目更改時, 數據源的 IDataErrorInfo 接口被查詢新值有效。
緣由,SelectedItem 屬性綁定具備 ValidatesOnDataErrors 設置爲 True。
因爲數據源是一個 customerViewModel 對象,綁定系統驗證錯誤要求該 customerViewModel 客戶類型屬性。
大多數狀況下,CustomerViewModel 委託客戶對象,它包含驗證錯誤的全部請求。
可是,由於客戶有 IsCompany 屬性的未選中的狀態的沒有概念,CustomerViewModel 類必須處理驗證 ComboBox 控件中新的選定的項目。
該代碼會出現 圖 14 中。
![](http://static.javashuo.com/static/loading.gif)
圖 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";
}
此代碼的關鍵的方面是 IDataErrorInfo CustomerViewModel 的實現能夠處理 ViewModel 特定屬性驗證的請求委派客戶對象在其餘請求。
這容許您使用的模型的類中的驗證邏輯和具備用於僅意義 ViewModel 類的屬性的附加驗證。
可以保存一個 CustomerViewModel 是可用於經過 SaveCommand 屬性的視圖。
該命令使用 RelayCommand 類以前檢查,以容許 CustomerViewModel 決定若是它能夠保存自己以及告訴保存其狀態時執行。
在此應用程序,保存新的客戶只是意味着將其添加到一個 CustomerRepository。
決定新的客戶是否已準備好保存須要雙方的許可。
客戶對象必須是要求是否它是有效的),或未,而在 customerViewModel 必須肯定它是否有效。
此兩部分決定是須要由於 ViewModel 特定屬性和檢查之前的驗證。
圖 15 顯示 CustomerViewModel 在保存邏輯。
![](http://static.javashuo.com/static/loading.gif)
圖 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 使得更易於建立視圖,能夠顯示客戶對象和等待等 Boolean 類型的值的屬性的"未選中"狀態。
它還提供可以很容易地判斷客戶保存其狀態。
若是直接對客戶對象綁定視圖,視圖將須要大量代碼來完成此工做正常。
設計良好的 MVVM 體系結構中大多數視圖的源代碼應爲空,或者,最,只包含操做控件和該視圖中包含資源的代碼。
有時也是須要在視圖的源代碼與如掛接事件 ViewModel 對象交互中編寫代碼,或者調用方法原本會很難從該 ViewModel 自己調用。
全部客戶都查看
演示應用程序還包含工做區,ListView 中顯示全部的客戶。
在列表客戶的分組方式根據指明它們是否公司或我的。
用戶能夠一次選擇一個或多個客戶,並在右下角中查看其總銷售額的總和。
在用戶界面是在 AllCustomersView 爲控件它們呈現 AllCustomersViewModel 對象。
每一個 ListViewitem 表明 CustomerViewModel 對象由 AllCustomerViewModel 對象公開 AllCustomers 集合中。
在上一的節您瞭解如何在 CustomerViewModel 能夠呈現爲數據輸入窗體,並徹底相同的 CustomerViewModel 對象在視爲 ListView 中的一個項目的呈現如今。
CustomerViewModel 該類具備不知道哪些可視元素顯示,這就是這種重用是可能的緣由。
AllCustomersView 建立 ListView 中的組。
這是經過綁定到 圖 16 像配置一個 collectionViewSource 的 ListView 的 ItemsSource 完成的。
![](http://static.javashuo.com/static/loading.gif)
圖 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>
ListView 的 ItemContainerStyle 屬性創建一個 ListViewItem 和 CustomerViewModel 對象之間的關聯。
樣式分配給該屬性應用於每一個 ListViewItem 使屬性在 CustomerViewModel 上綁定到屬性的 ListViewItem。
該樣式中的一個重要綁定會建立一個 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 監視每一個客戶所選擇或取消選擇和通知須要更新顯示值的視圖。
![](http://static.javashuo.com/static/loading.gif)
圖 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");
}
在用戶界面將綁定到 TotalSelectedSales 屬性,並應用貨幣格式值 (貨幣)。
ViewModel 對象能夠經過從 TotalSelectedSales 屬性返回一個 Double 值而不是字符串將貨幣格式而不是在的視圖,應用。
ContentPresenter 的該 ContentStringFormat 屬性在.NET Framework 3.5 SP 1 中, 添加時是所以若是您必須爲目標舊版本的 WPF,您須要應用貨幣格式代碼中:
<!-- In AllCustomersView.xaml -->
<StackPanel Orientation="Horizontal">
<TextBlock Text="Total selected sales: " />
<ContentPresenter
Content="{Binding Path=TotalSelectedSales}"
ContentStringFormat="c"
/>
</StackPanel>
向上覆蓋
WPF 有不少提供應用程序開發,並利用該功能的學習須要思惟方式 Shift。
模型-視圖-ViewModel 模式是一種簡單而有效的組的設計和實現 WPF 應用程序的指南。
它將容許您建立強分隔數據、 行爲和以便更易於控制是軟件開發的混亂的演示文稿。
我想感謝 John Gossman 有關本文的他幫助。
有關使用 WPF 建立出色用戶體驗於 Josh Smith 。
他已授予 Microsoft MVP 標題的 WPF 社區中其工時。
Josh 適用 Infragistics 體驗設計組中。
當他不在計算機中時,他受播放該的鋼琴與他 girlfriend 閱讀有關歷史記錄和瀏覽紐約城市。
您能夠訪問在 Josh 的博客
joshsmithonwpf.wordpress.com .