WPF MVVM學習
轉自https://my.oschina.net/unpluggedcoder/blog/536301
簡介
簡單的三層架構示例和 GLUE(膠水)代碼問題
第一步:最簡單的 MVVM 示例 - 把後臺代碼移到類中
第二步:添加綁定 - 消滅後臺代碼
第三步:添加執行動作和“INotifyPropertyChanged”接口
第四步:在 ViewModel 中解耦執行動作
第五步:利用 PRISM
WPF MVVM 的視頻演示
簡介
從我們還是兒童到學習成長為成年人,生命一直都在演變。 對於軟件架構, 同樣適用這個道理, 從一個基礎的架構開始, 隨著每個需求和情境在不斷演化。
如果你問任何一個 .NET 開發者, 什麽是最小的基礎架構, 首先浮現的就是"三層架構"。 在這個框架中, 我們把項目分為三個邏輯層次: UI 層, 業務邏輯層和數據訪問層, 每一層都負責各自對應的功能。
UI 負責顯示功能, 業務邏輯層負責校驗, 數據訪問層負責 SQL 語句。3層架構有如下的好處:
-
包容變化: 每一層的變化不會重復跨越到其它層次。
-
重用性: 增強可重用性, 因為每一層都是分離, 自包容的獨立實體
MVVM 是三層架構的一個演化。我知道我的經歷不夠證明這點, 但是我個人對 MVVM 進行了演化和觀察。 那我們先從三層基礎架構開始, 去理解三層架構存在的問題, 看 MVVM 架構是如何解決這些問題, 然後升級到去創建一個自定義的 MVVM 框架代碼。 下面是本文接下來的路線圖。
簡單的三層架構示例和 GLUE(膠水) 代碼問題
首先, 讓我們來理解三層架構以及它存在的問題, 然後看 MVVM 如何解決這個問題。
直覺和現實是兩種不同的事物。 當你看到三層架構的圖, 你首先的直覺是每個功能可能都分布在各自層次。 但是當你實際編寫代碼時, 有些層次被強迫去做一些它們不應該做的額外的工作(破壞了SOLID 原則)。 如果你對 SOLID 原則還不熟悉可以參考這個視頻: SOLID principle video(譯者註: SOLID 指 Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion, 即單一功能、開閉原則、裏氏替換、接口隔離以及依賴反轉)。
這部分額外工作就在 UI 與Model之間, 以及 Model 與 Data access 之間。 我們把這類代碼稱為"GLUE"(膠水, 譯者註:由於作者全用大寫字母表示, 因此後續延用 GLUE)代碼。"GLUE"代碼主要有兩種邏輯類型。
鄙人淺見薄識, 如果你有更多的"GLUE"類型實例, 請在留言中指出。
-
映射邏輯(綁定邏輯): 每一層通過屬性、方法和集合和其它層鏈接。例如, 一個在 UI 層中名為“txtCustomerName”的 Textbox 控件,將其映射到 customer 類的"CustomerName"屬性。
txtCustomerName.text = custobj.CustomerName; // 映射代碼
現在誰應該擁有上述綁定邏輯代碼,UI 還是 Model?開發者往往把這個代碼推到 UI 層次中。
-
轉換邏輯:每個層次使用的數據格式都是不同的。比如一個 Model 類"Person"有一個性別屬性,可取值分別為 "F"(Female) 和 "M"(Male) 分別代表女性和男性。但是在 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; // mapping code lblAmount.Content = o.Amount; // mapping code if (o.Amount > 2000) // transformation code { lblBuyingHabits.Background = new SolidColorBrush(Colors.Blue); } else if (o.Amount > 1500) // transformation code { lblBuyingHabits.Background = new SolidColorBrush(Colors.Red); } if (obj.Married == "Married") // transformation code { chkMarried.IsChecked = true; } else { chkMarried.IsChecked = false; }
現在這些 GLUE 代碼存在的問題:
-
單一責任原則被破壞(SRPViolation): 是 UI 負責這些 GLUE 代碼嗎?這種情況下改變了 Amount 數量,同時也需要修改 UI 代碼。現在,數據的改變為什麽會讓我去修改 UI 的代碼?這裏可以聞到壞代碼的味道。UI 應該只在我修改樣式,顏色和布局的時候才改變。
-
重用性: 如果我想把同樣的顏色邏輯和性別格式轉換用到下面的編輯界面,我該怎麽做?拷貝粘帖重復的代碼?
如果我想走得更遠一點,把這個 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”這個類有以下幾點註意:
-
類屬性都以 UI 的命名方式來約定,這樣看上去會更形象一些;
-
這個類負責了類型轉換的代碼,使得 UI 看上去更輕量級。例如代碼中的“TxtAmount”屬性。在 Model 類中的“Amount”屬性是數字,而轉換的過程是在 ViewModel 類中完成。換句話說這個類負責了 UI 顯示的所有職責(譯者註:邏輯上的業務職責)讓 UI 後臺代碼看上去更簡潔;
-
所有轉換邏輯的代碼都在這個類中,例如“LblAmountColor”屬性和“IsMarried”屬性;
-
所有的屬性數據都保持了簡單的字符類型,這樣可以在大多 UI 技術平臺上適用。例如,“LblAmountColor”屬性把顏色值用字符串來傳遞,這樣可以在任何 UI 類型中重用,同時我們也保持了最小的數據共性。
現在“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# 代碼有三個步驟:
-
導入:我們要做的第一件事情是導入“CustomerViewModel”名稱空間。
-
創建對象:下一步要創建“CustomerViewModel”類的對象。
-
綁定代碼:最後將 WPF UI 綁定到這個 ViewModel 對象。
下面表格展示了 C# 代碼和與其對應相同的 WPF XAML 代碼。
C# code | XAML code | |
Import | using CustomerViewModel; | xmlns:custns="clr- namespace:CustomerViewModel;assembly=Custo merViewModel" |
Create object |
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”/>
|
Bind | 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 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 類的引用)。
下面是簡短的代碼片段,有四點需要註意:
-
ViewModel 對象是作為一個私有的成員對象。
-
該 ViewModel 對象將通過構造函數參數的方式傳遞進來。
-
目前為止,我們沒有在“CanExecute”中添加驗證邏輯,它始終返回 true。
-
在“Execute”方法中我們調用了 ViewModel 類的“Calculate”方法。
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 類的要點:
-
command 類是“CustomerViewModel”類的私有成員。
-
在“CustomerViewModel”類的構造函數中將當前對象的實例傳遞給 command 類。在之前解釋 command 類的一節中我們說了 command 類構造函數獲取 ViewModel 類的實例。因此在這一節中我們正是將當前實例傳遞給 command 類。
-
command 對象是通過以“ICommand”接口的形式暴露出來,這樣才可以被 XAML 所使用。
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”都可以用來創建委托。
通過使用委托的方法,我們試著創建一個通用的 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學習