本文翻譯自Shivprasad koirala在CodeProject上的文章:WPF MVVM step by step (Basics to Advance Level)php
###簡介 從咱們仍是兒童到學習成長爲成年人, 生命一直都在演變。 對於軟件架構, 一樣適用這個道理, 從一個基礎的架構開始, 隨着每一個需求和情境在不斷演化。html
若是你問任何一個.NET開發者, 什麼是最小的基礎架構, 首先浮現的就是"三層架構"。 在這個框架中, 咱們把項目分爲三個邏輯層次: UI層, 業務邏輯層和數據訪問層, 每一層都負責各自對應的功能。編程
UI負責顯示功能, 業務邏輯層負責校驗, 數據訪問層負責SQL語句。 3層架構有以下的好處:c#
MVVM是三層架構的一個演化。 我知道我沒有一個歷史去證實這點, 可是我我的對MVVM進行了演化和觀察。 那咱們先從三層基礎架構開始, 去理解三層架構存在的問題, 看MVVM架構是如何解決這些問題, 而後升級到去建立一個自定義的MVVM框架代碼。 下面是本文接下來的路線圖。安全
###簡單的三層架構示例和GLUE(膠水)代碼問題多線程
首先, 讓咱們來理解三層架構以及它存在的問題, 而後看MVVM如何解決這個問題。架構
直覺和現實是兩種不一樣的事物。 當你看到三層架構的圖, 你首先的直覺是每一個功能可能都分佈在各自層次。 可是當你實際編寫代碼時, 有些層次被強迫去作一些它們不該該作的額外的工做(破壞了SOLID原則)。 若是你對SOLID原則還不熟悉能夠參考這個視頻: SOLID principle video(譯者注: SOLID指Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion, 即單一功能、開閉原則、里氏替換、接口隔離以及依賴反轉)。app
這部分額外工做就在UI與Model之間, 以及Model與Data access之間。 咱們把這類代碼稱爲"GLUE"(膠水, 譯者注:因爲做者全用大寫字母表示, 所以後續延用GLUE)代碼。 "GLUE"代碼主要有兩種邏輯類型: 鄙人淺見薄識, 若是你有更多的"GLUE"類型實例, 請在留言中指出。框架
txtCustomerName.text = custobj.CustomerName; // 映射代碼
如今誰應該擁有上述綁定邏輯代碼,UI仍是Model?開發者每每把這個代碼推到UI層次中。異步
if (obj.Gender == 「M」) // 轉換代碼 {chkMale.IsChecked = true;} else {chkMale.IsChecked = false;}
大多數開發者最終會將"GLUE"代碼寫到UI層中。一般能夠在後臺代碼中定位到這類代碼,例如.cs文件。若是UI是XAML,則對應的XAML.cs包含GLUE代碼;若是UI是ASPX,則對應的ASPX.cs包含GLUE代碼,以此類推。
那麼問題來了:是UI負責這類GLUE代碼嗎?讓咱們看下WPF應用中的一個簡單的三層結構例子,以及更詳細的GLUE代碼細節。
下面是一個簡單的模型類"Customer",它有三個屬性「CustomerName」, 「Amount」 和「Married」 。
可是,當這個模型顯示到UI上時它又表現以下。因此,你能夠看出來它包含了該模型的全部屬性,以及一些額外的元素:顏色標籤和Married複選框控件。
下面有一張簡單的表,左邊是Model,右邊是UI,中間是談過的映射和轉換邏輯。
你能夠看到前兩行沒有轉換邏輯,只有映射邏輯,另外兩行則同時包含轉換邏輯和映射邏輯。
Model | GLUE CODE | UI |
---|---|---|
Customer Name | No conversion needed only Mapping | Customer Name |
Amount | No conversion needed only Mapping | Amount |
Amount | Mapping + Conversion logic. | > 1500 = BLUE, < 1500 = RED |
Married | Mapping + Conversion logic. | True – Married, False - UnMarried |
這些轉換和映射邏輯代碼一般會在「xaml.cs」文件中。下面是上圖對應的後臺代碼,你能夠看到映射代碼和顏色斷定、性別格式轉換代碼。我在代碼中用註釋標註出來,這樣你能夠看到哪些是映射代碼,哪些是轉換代碼。
lblName.Content = o.CustomerName; // 映射代碼 lblAmount.Content = o.Amount; // 映射代碼 if (o.Amount > 2000) // 轉換代碼 { lblBuyingHabits.Background = new SolidColorBrush(Colors.Blue); } else if (o.Amount > 1500) // 轉換代碼 { lblBuyingHabits.Background = new SolidColorBrush(Colors.Red); } if (obj.Married == "Married") // 轉換代碼 { chkMarried.IsChecked = true; } else { chkMarried.IsChecked = false; }
如今這些GLUE代碼存在的問題:
若是我想走得更遠一點,把這個GLUE代碼用在不一樣的UI技術體系上,好比MVC、Windows Form或者Mobile應用上。
可是這裏跨UI技術平臺的重用其實是不可能的,由於每一個平臺UI背後都和各自的UI技術體系耦合得很緊密。
好比,下面的後臺代碼是繼承自「Windows」類,而「Windows」類是集成在WPF UI體系中。若是咱們想在Web應用或者MVC中應用這些邏輯,卻又沒法去建立一個這樣的類對象來使用。
public partial class MainWindow : Window { // Behind code is here }
那麼咱們要怎麼重用後臺代碼?怎麼遵循SRP原則?
###第一步:最簡單的MVVM示例 - 把後臺代碼移到類中
我想大部分開發者已經知道怎麼解決這個問題。毫無疑問地把後臺代碼(GLUE代碼)移到一個類庫中。這個類庫表明了描述了UI的屬性和行爲。任何移入到這個類庫的代碼均可以編譯成DLL,而後被全部.NET項目(Windows, Web等等)所引用。 所以,在這一節咱們將建立一個最簡單的MVVM示例,而後在後續的章節中咱們將基於這個示例建立更高級的MVVM示例。
咱們建立一個「CustomerViewModel」類來包含GLUE代碼。「CustomerViewModel」類表明了你的UI,因此咱們想保持它的屬性和UI命名約定一致。你能夠從下圖看出來「CustomerViewModel」類的屬性是如何從以前的CustomerModel類中映射過來: 「TxtCustomerName」對應「CustomerName」,「TxtAmount」對應「Amount」等等。
下面是實際代碼:
public class CustomerViewModel { private Customer obj = new Customer(); public string TxtCustomerName { get { return obj.CustomerName; } set { obj.CustomerName = value; } } public string TxtAmount { get { return Convert.ToString(obj.Amount) ; } set { obj.Amount = Convert.ToDouble(value); } } public string LblAmountColor { get { if (obj.Amount > 2000) { return "Blue"; } else if (obj.Amount > 1500) { return "Red"; } return "Yellow"; } } public bool IsMarried { get { if (obj.Married == "Married") { return true; } else { return false; } } }}
關於「CustomerViewModel」這個類有如下幾點注意:
如今「CustomerViewModel」類包含了全部的後臺代碼邏輯,咱們能夠建立這個類的對象並綁定到UI元素上。你能夠在下面代碼看到咱們只剩下了映射邏輯的代碼部分,而轉換邏輯的"GLUE"代碼已經沒有了。
private void DisplayUi(CustomerViewModel o) { lblName.Content = o.TxtCustomerName; lblAmount.Content = o.TxtAmount; BrushConverter brushconv = new BrushConverter(); lblBuyingHabits.Background = brushconv.ConvertFromString(o.LblAmountColor) as SolidColorBrush; chkMarried.IsChecked = o.IsMarried; }
###第二步:添加綁定 - 消滅後臺代碼
第一步的方法很好,可是咱們知道後臺代碼仍然還有問題,在WPF中消滅全部後臺代碼是徹底可能的。接下來WPF綁定和命令登場了。
WPF以其綁定(Binding)、命令(Commands)和聲明式編程(Declarative programming)而著稱。聲明式編程意味着你可使用XMAL來表達你的C#代碼,而不用編寫完整的C#代碼。綁定功能幫助一個WPF對象鏈接到其它的WPF對象,從而他們能夠發送和接收數據。
當前的映射C#代碼有三個步驟:
下面表格展現了C#代碼和與其對應相同的WPF XAML代碼。
步驟 | C#代碼 | XAML代碼 |
---|---|---|
導入 | using CustomerViewModel; | xmlns:custns="clr-namespace:CustomerViewModel;assembly=CustomerViewModel" |
建立對象 | CustomerViewModelobj = new CustomerViewModel(); obj.CustomerName = "Shiv"; obj.Amount = 2000; obj.Married = "Married"; | < Window.Resources> < custns: CustomerViewModel x:Key="custviewobj" TxtCustomerName="Shiv" TxtAmount="1000" IsMarried=」true」/> |
綁定對象 | lblName.Content = o.CustomerName; | < Label x:Name="lblName" Content="{Binding TxtCustomerName, Source={StaticResourcecustviewobj}}"/> |
你不須要寫後臺的代碼,咱們能夠選中UI元素,按F4,以下圖中選擇指定綁定。這個步驟會把綁定代碼插入到XAML中。
選擇「StaticResource」來指定映射,而後在UI元素和ViewModel對象之間指定綁定路徑。
這是你查看XAML.CS文件,它已經沒有任何GLUE代碼,一樣也沒有轉換和映射代碼。惟一的代碼就是標準的WPF UI初始化代碼。
public partial class MVVMWithBindings : Window { public MVVMWithBindings() {InitializeComponent();} }
###第三步:添加執行動做和「INotifyPropertyChanged」接口
應用程序不只僅只是有textboxs 和 labels, 一樣還須要執行動做,好比按鈕,鼠標事件等。 所以讓咱們添加一個按鈕來看看如何把MVVM類應用起來。 咱們在一樣的UI上添加了一個‘Calculate tax’按鈕,當用戶按下按鈕,它將根據「Sales Amount」值計算出稅值並顯示在界面上。
所以爲了在Model類實現上面的功能,咱們添加一個「CalculateTax()」方法。當這個方法被執行,它根據薪水範圍計算出稅值,並將值保存在「Tax」屬性值中。
public class Customer { .... .... .... .... private double _Tax; public double Tax { get { return _Tax; } } public void CalculateTax() { if (_Amount > 2000) { _Tax = 20; } else if (_Amount > 1000) { _Tax = 10; } else { _Tax = 5; } } }
因爲ViewModel類是Model類的一個封裝,所以咱們須要在ViewModel類中建立一個方法來調用Model的「CalculateTax」方法。
public class CustomerViewModel { private Customer obj = new Customer(); .... .... .... .... public void Calculate() { obj.CalculateTax(); } }
如今,咱們想要在XAML的視圖中調用這個「Calculate」方法,而不是在後臺編寫。不過你不能直接經過XAML調用「Calculate」方法,你須要用WPF的command類。
咱們經過使用綁定屬性將數據發送給ViewModel類,而發送執行動做給ViewModel類則須要使用命令。
全部從視圖元素產生的動做都發送給command類,因此第一步是建立一個command類。爲了建立自定義的command類,咱們須要實現"ICommand"接口(以下圖)。
"ICommand"接口有兩個必需要重載的方法:「CanExecute」 和 「Execute」。在「Execute」中咱們放的是但願動做發生時實際執行的邏輯代碼(好比按鈕按下,右鍵按下等)。在「CanExecute」中咱們放的是驗證邏輯來決定「Execute」代碼是否應該執行。
public class ButtonCommand : ICommand { public bool CanExecute(object parameter) { // When to execute // Validation logic goes here } public event EventHandler CanExecuteChanged; public void Execute(object parameter) { // What to Execute // Execution logic goes here } }
如今全部的動做調用都發送到command類,而後被路由到ViewModel類。換句話說,command類須要組合ViewModel類(譯註:command類須要一個ViewModel類的引用)。
下面是簡短的代碼片斷,有四點須要注意:
public class ButtonCommand : ICommand { private CustomerViewModel obj; // Point 1 public ButtonCommand(CustomerViewModel _obj) // Point 2 { obj = _obj; } public bool CanExecute(object parameter) { return true; // Point 3 } public void Execute(object parameter) { obj.Calculate(); // Point 4 } }
上面的command代碼中,ViewModel對象是經過構造函數傳遞進來。因此ViewModel類須要建立一個command對象來暴露這個對象的「ICommand」接口。這個「ICommand」接口將被WPF XAML使用並調用。下面是一些關於「CustomerViewModel」類使用command類的要點:
using System.ComponentModel; public class CustomerViewModel { … … private ButtonCommand objCommand; // Point 1 public CustomerViewModel() { objCommand = new ButtonCommand(this); // Point 2 } public ICommand btnClick // Point 3 { get { return objCommand; } } … … }
在你的UI中添加一個按鈕,這樣就能夠把按鈕的執行動做鏈接到暴露的「ICommand」接口。如今打開button的屬性欄,選擇command屬性,右擊建立一個數據綁定。
而後選擇靜態資源(Static Resource),並將「ButtonCommand」附加到button上。
當你點擊了Calculate Tax按鈕,它就執行了「CalculateTax」方法。並將稅值結果存在「_tax」變量中。關於「CalculateTax」方法代碼,能夠閱讀前面的小節「第三步:添加執行動做和「INotifyPropertyChanged」接口」。
換句話說,稅值計算過程並不會自動通知給UI。因此咱們須要從對象發送某種通知給UI,告訴它稅值已經變化了,UI須要從新載入綁定值。
所以,在ViewModel類中咱們須要發送INotify事件給視圖。
爲了讓你的ViewModel類可以實現通知,咱們必須作三件事情。這三件事情都在下面的代碼註釋中指出,例如Point1, Point2 和 Point3。
Point1: 以下面代碼那樣實現「INotifyPropertyChanged」接口。一旦你實現了該接口,它就建立了對象的「PropertyChangedEventHandler」事件。
Point2和3: 在「Calculate」方法中用「PropertyChanged」對象去觸發事件,並在其中指定了某個屬性的通知。在這裏是「Tax」屬性。安全起見,咱們一樣也要檢查「PropertyChanged」是否不爲空。
public class CustomerViewModel : INotifyPropertyChanged // Point 1 { …. …. public void Calculate() { obj.CalculateTax(); if (PropertyChanged != null) // Point 2 { PropertyChanged(this,new PropertyChangedEventArgs("Tax")); // Point 3 } } public event PropertyChangedEventHandler PropertyChanged; }
若是你運行程序,你應該能夠看見當點擊按鈕後「Tax」值被更新了。
###第四步:在ViewModel中解耦執行動做
到目前爲止,咱們用MVVM框架建立了一個簡單的界面。這個界面同時包含了屬性和命令實現。咱們擁有了一個視圖,它的UI輸入元素(例如textbox)經過綁定和ViewModel鏈接起來,它的任何執行動做(例如按鈕點擊)經過命令和ViewModel鏈接起來。ViewModel和內部的Model通信。
可是在上面的結構中還有一個問題:command類和ViewModel類存在着過分耦合的狀況。若是你還記得command類代碼(我在下面貼出來了)中的構造函數是傳遞了ViewModel對象,這意味着這個command類沒法被其它的ViewModel類所複用。
public class ButtonCommand : ICommand { private CustomerViewModel obj; // Point 1 public ButtonCommand(CustomerViewModel _obj) // Point 2 { obj = _obj; } ...... ...... ...... }
可是在考慮了全部狀況以後,讓咱們邏輯地思考下「什麼是一個動做?」。它是一個事件,能夠由用戶從鼠標點擊(左鍵或右鍵),按鈕點擊,菜單點擊,功能鍵按下等。因此應該有一種方式通用化這些動做,而且讓各類ViewModel有一種更通用的方法去綁定它。
邏輯上講,若是你認爲任務動做是一些方法和函數的封裝邏輯。那有什麼是「方法」和「函數」的通用表達方式呢?......努力想一想.......再想一想.......「委託」,「委託」,沒錯,仍是「委託」。
咱們須要兩個委託,一個給「CanExecute」,另外一個給「Execute」。「CanExecute」返回一個布爾值用來驗證以及根據驗證來使能(Enable)或者禁用(Disable)用戶界面。「Execute」委託則將在「CanExecute」委託返回true時執行。
public class ButtonCommand : ICommand { public bool CanExecute(object parameter) // Validations { } public void Execute(object parameter) // Executions { } }
所以,換句話說,咱們須要兩個委託,一個返回布爾值,另外一個執行動做並返回空。因此,建立一個「Func」和一個「Action」如何?「Func」和「Action」均可以用來建立委託。
若是你還不熟悉Func和Action,能夠看下下面這個視頻。 (譯註:做者在這裏提供了一個YouTube的視頻連接,大概說的就是C#中Func<>和Action<>這兩個委託的區別,前者Func<>模版參數包含返回值類型,而Action<>表示無返回值的泛型委託,參見這裏)
經過使用委託的方法,咱們試着建立一個通用的command類。咱們對command類作了三個修改(代碼參見下面),同時我也標註了三點Point 1,2和3。
Point1: 咱們在構造函數中移除了ViewModel對象,改成接受兩個委託,一個是「Func」,另外一個是「Action」。「Func」委託用做驗證(例如驗證什麼時候動做將被執行),而「Action」委託用來執行動做。兩個委託都是經過構造函數參數傳遞進來,並賦值給類內部的對應私有成員變量。
Point2和3: Func<>委託(WhentoExecute)被「CanExecute」調用,執行動做的委託Whattoexecute則是在「Execute」中被調用。
public class ButtonCommand : ICommand { private Action WhattoExecute; private Func<bool> WhentoExecute; public ButtonCommand(Action What , Func<bool> When) // Point 1 { WhattoExecute = What; WhentoExecute = When; } public bool CanExecute(object parameter) { return WhentoExecute(); // Point 2 } public void Execute(object parameter) { WhattoExecute(); // Point 3 } }
在Model類中咱們已經知道要執行什麼了(例如「CalculateTax」),咱們也建立一個簡單的函數「IsValid」來驗證「Customer」類是否有效。
public class Customer { public void CalculateTax() { if (_Amount > 2000) { _Tax = 20; } else if (_Amount > 1000) { _Tax = 10; } else { _Tax = 5; } } public bool IsValid() { if (_Amount == 0) { return false; } else { return true; } } }
在ViewModel類中咱們同時傳遞函數和方法給command類的構造函數,一個給「Func」,一個給「Action」。
public class CustomerViewModel : INotifyPropertyChanged { private Customer obj = new Customer(); privateButtonCommandobjCommand; publicCustomerViewModel() { objCommand = new ButtonCommand(obj.CalculateTax, obj.IsValid); } }
這樣使得框架更好,更解耦, 使得這個command類能夠以一個通用的方式被其它ViewModel引用。下面是改善後的架構, 須要注意ViewModel如何經過委託(Func和Action)和command類交互。
###第五步:利用PRISM
最後若是有一個框架能幫助實現咱們的MVVM代碼那就更好了。PRISM就是其中一個可複用的框架。PRISM的主要用途是爲了提供模塊化開發,可是它提供了一個很好的「DelegateCommand」類拿來代替咱們本身建立的command類。
因此,第一件事情就是從這裏下載PRISM,編譯這個解決方案,添加「Microsoft.Practices.Prism.Mvvm.dll」和「Microsoft.Practices.Prism.SharedInterfaces.dll」這兩個DLL庫的引用。
你能夠去掉自定義的command類,導入「Microsoft.Practices.Prism.Commands」名稱空間, 而後如下面代碼的方式使用DelegateCommand。
public class CustomerViewModel : INotifyPropertyChanged { private Customer obj = new Customer(); private DelegateCommand objCommand; public CustomerViewModel() { objCommand = new DelegateCommand(obj.CalculateTax, obj.IsValid); } ………… ………… ………… ………… } }
###WPF MVVM的視頻演示
我同時也在下面的視頻中從頭演示瞭如何實現WPF MVVM(譯註:一個YouTube連接...)。
###延伸閱讀