[譯] WPF MVVM 按部就班(從基礎到高級)

本文翻譯自Shivprasad koirala在CodeProject上的文章:WPF MVVM step by step (Basics to Advance Level)php


###簡介 從咱們仍是兒童到學習成長爲成年人, 生命一直都在演變。 對於軟件架構, 一樣適用這個道理, 從一個基礎的架構開始, 隨着每一個需求和情境在不斷演化。html

若是你問任何一個.NET開發者, 什麼是最小的基礎架構, 首先浮現的就是"三層架構"。 在這個框架中, 咱們把項目分爲三個邏輯層次: UI層, 業務邏輯層和數據訪問層, 每一層都負責各自對應的功能。編程

三層架構

UI負責顯示功能, 業務邏輯層負責校驗, 數據訪問層負責SQL語句。 3層架構有以下的好處:c#

  • 包容變化: 每一層的變化不會重複跨越到其它層次。
  • 重用性: 加強可重用性, 由於每一層都是分離, 自包容的獨立實體

MVVM是三層架構的一個演化。 我知道我沒有一個歷史去證實這點, 可是我我的對MVVM進行了演化和觀察。 那咱們先從三層基礎架構開始, 去理解三層架構存在的問題, 看MVVM架構是如何解決這些問題, 而後升級到去建立一個自定義的MVVM框架代碼。 下面是本文接下來的路線圖。安全

Road map of MVVM

###簡單的三層架構示例和GLUE(膠水)代碼問題多線程

首先, 讓咱們來理解三層架構以及它存在的問題, 而後看MVVM如何解決這個問題。架構

直覺和現實是兩種不一樣的事物。 當你看到三層架構的圖, 你首先的直覺是每一個功能可能都分佈在各自層次。 可是當你實際編寫代碼時, 有些層次被強迫去作一些它們不該該作的額外的工做(破壞了SOLID原則)。 若是你對SOLID原則還不熟悉能夠參考這個視頻: SOLID principle video(譯者注: SOLID指Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion, 即單一功能、開閉原則、里氏替換、接口隔離以及依賴反轉)。app

GLUE Code

這部分額外工做就在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」 。

Customer class

可是,當這個模型顯示到UI上時它又表現以下。因此,你能夠看出來它包含了該模型的全部屬性,以及一些額外的元素:顏色標籤和Married複選框控件。

Customer UI

下面有一張簡單的表,左邊是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代碼存在的問題:

  • 單一責任原則被破壞(SRPViolation): 是UI負責這些GLUE代碼嗎?這種狀況下改變了Amount數量,同時也須要修改UI代碼。如今,數據的改變爲何會讓我去修改UI的代碼?這裏能夠聞到壞代碼的味道。UI應該只在我修改樣式,顏色和佈局的時候才改變。
  • 重用性: 若是我想把一樣的顏色邏輯和性別格式轉換用到下面的編輯界面,我該怎麼作?拷貝粘帖重複的代碼?

CustomerEdit

若是我想走得更遠一點,把這個GLUE代碼用在不一樣的UI技術體系上,好比MVC、Windows Form或者Mobile應用上。

Reusability

可是這裏跨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示例。

Simplest MVVM

咱們建立一個「CustomerViewModel」類來包含GLUE代碼。「CustomerViewModel」類表明了你的UI,因此咱們想保持它的屬性和UI命名約定一致。你能夠從下圖看出來「CustomerViewModel」類的屬性是如何從以前的CustomerModel類中映射過來: 「TxtCustomerName」對應「CustomerName」,「TxtAmount」對應「Amount」等等。

ViewModel

下面是實際代碼:

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#代碼 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中。

Create Binding1

選擇「StaticResource」來指定映射,而後在UI元素和ViewModel對象之間指定綁定路徑。

Create Binding2

這是你查看XAML.CS文件,它已經沒有任何GLUE代碼,一樣也沒有轉換和映射代碼。惟一的代碼就是標準的WPF UI初始化代碼。

public partial class MVVMWithBindings : Window
{
        public MVVMWithBindings()
        {InitializeComponent();}
}

###第三步:添加執行動做和「INotifyPropertyChanged」接口

應用程序不只僅只是有textboxs 和 labels, 一樣還須要執行動做,好比按鈕,鼠標事件等。 所以讓咱們添加一個按鈕來看看如何把MVVM類應用起來。 咱們在一樣的UI上添加了一個‘Calculate tax’按鈕,當用戶按下按鈕,它將根據「Sales Amount」值計算出稅值並顯示在界面上。

Add Action

所以爲了在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類則須要使用命令。

Action and Properties

全部從視圖元素產生的動做都發送給command類,因此第一步是建立一個command類。爲了建立自定義的command類,咱們須要實現"ICommand"接口(以下圖)。

"ICommand"接口有兩個必需要重載的方法:「CanExecute」 和 「Execute」。在「Execute」中咱們放的是但願動做發生時實際執行的邏輯代碼(好比按鈕按下,右鍵按下等)。在「CanExecute」中咱們放的是驗證邏輯來決定「Execute」代碼是否應該執行。

ICommand

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類的引用)。

Route

下面是簡短的代碼片斷,有四點須要注意:

  1. ViewModel對象是做爲一個私有的成員對象。
  2. 該ViewModel對象將經過構造函數參數的方式傳遞進來。
  3. 目前爲止,咱們沒有在「CanExecute」中添加驗證邏輯,它始終返回true。
  4. 在「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類的要點:

  1. command類是「CustomerViewModel」類的私有成員。
  2. 在「CustomerViewModel」類的構造函數中將當前對象的實例傳遞給command類。在以前解釋command類的一節中咱們說了command類構造函數獲取ViewModel類的實例。所以在這一節中咱們正是將當前實例傳遞給command類。
  3. 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屬性,右擊建立一個數據綁定。

Button Property

而後選擇靜態資源(Static Resource),並將「ButtonCommand」附加到button上。

Command Binding

當你點擊了Calculate Tax按鈕,它就執行了「CalculateTax」方法。並將稅值結果存在「_tax」變量中。關於「CalculateTax」方法代碼,能夠閱讀前面的小節「第三步:添加執行動做和「INotifyPropertyChanged」接口」。

換句話說,稅值計算過程並不會自動通知給UI。因此咱們須要從對象發送某種通知給UI,告訴它稅值已經變化了,UI須要從新載入綁定值。

Notification

所以,在ViewModel類中咱們須要發送INotify事件給視圖。

Notification

爲了讓你的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通信。

Simple MVVM

可是在上面的結構中還有一個問題:command類和ViewModel類存在着過分耦合的狀況。若是你還記得command類代碼(我在下面貼出來了)中的構造函數是傳遞了ViewModel對象,這意味着這個command類沒法被其它的ViewModel類所複用。

public class ButtonCommand : ICommand
    {
        private CustomerViewModel obj; // Point 1
        public ButtonCommand(CustomerViewModel _obj) // Point 2
        {
            obj = _obj;
        }
......
......
......

}

More Actions

可是在考慮了全部狀況以後,讓咱們邏輯地思考下「什麼是一個動做?」。它是一個事件,能夠由用戶從鼠標點擊(左鍵或右鍵),按鈕點擊,菜單點擊,功能鍵按下等。因此應該有一種方式通用化這些動做,而且讓各類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類交互。

Final architecture

###第五步:利用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連接...)。

IMAGE ALT TEXT

###延伸閱讀

  1. WPF/MVVM Quick Start Tutorial
  2. Simplifying the WPF TreeView by Using the ViewModel Pattern
  3. MVVM 應用程序中的多線程與調度
  4. 針對異步 MVVM 應用程序的模式:數據綁定
  5. 針對異步 MVVM 應用程序的模式:命令
  6. Using behaviours to bind to read-only properties in MVVM
  7. Cascading ComboBoxes in WPF using MVVM
  8. WPF/MVVM: Binding the IsChecked Property of a CheckBox to Several Other CheckBoxes
相關文章
相關標籤/搜索