[MVVM專題]__MSDN上最詳細的介紹和剖析(上)

Model-View-ViewModel (MVVM) 設計模式描述了建立Windows Presentation Foundation 或 Microsoft Silverlight 應用程序的常用方法。Robert McCarter 演示了 ViewModel 的工作原理,並討論了用您的代碼實現 ViewModel 的優缺點。

     PS: 本文來自MSDN,由Robert McCarter大神所寫,具體地址已經未知,我只是將其排下版轉載過來而已不過本文從各個方面詳細地介紹了MVVM如何運作與實現.實在是一篇不可多得的好文

 

      Windows Presentation Foundation (WPF) 和 Silverlight 提供了豐富的 API 用來構建現代應用程序,但是瞭解並和諧一致地應用所有 WPF 特性來構建設計精良、易於維護的應用程序可能非常困難。從何處入手?什麼樣的方法纔算是正確的應用程序設計方法?

      Model-View-ViewModel (MVVM) 設計模式描述了構建 WPF 或 Silverlight 應用程序的常用方法。它還是一款構建應用程序的強大工具,以及一種與開發人員討論應用程序設計的通用語言。雖然 MVVM 確實很有用,但它發展時間不長,用戶尚未形成正確的認識。

      MVVM 設計模式什麼時候是適用的,什麼時候又是不必要的?應該如何設計應用程序的結構?ViewModel 層有多少代碼要編寫和維護,有什麼替代方式能夠減少 ViewModel 層的代碼量?如何妥善處理 Model 中的相關屬性?應該如何在 View 中顯示 Model 中的集合?應該在哪裏實例化 ViewModel 對象,並將其掛接到 Model 對象?

       在本文中,我將解釋 ViewModel 的工作原理,並討論在您的代碼中實現 ViewModel 的優缺點。我還會介紹一些具體的示例,演示如何使用 ViewModel 作爲文檔管理器,以便在 View 層中顯示 Model 對象。

------------------------------------------------

Model、ViewModel 和 View

     到目前爲止,我設計過的每個 WPF 和 Silverlight 應用程序都具有相同的高層組件設計。Model 是應用程序的核心,需要投入大量精力,按照面向對象的分析和設計 (OOAD) 最佳做法進行設計。

     對我來說,Model 是應用程序的核心,代表着最大、最重要的業務資產,因爲它記錄了所有複雜的業務實體、它們之間的關係以及它們的功能。

     Model 之上是 ViewModel。ViewModel 的兩個主要目標分別是:使 Model 能夠輕鬆被 WPF/XAML View 使用;將 Model 從 View 分離並對 Model 進行封裝。這些目標當然非常好,但是由於一些現實的原因,有時並不能達到這些目標。

      您構建的 ViewModel 知道用戶在高層上將如何與應用程序交互。但是,ViewModel 對 View 一無所知,這是 MVVM 設計模式的重要部分。這使得交互設計師和圖形設計師能夠在 ViewModel 的基礎上創建優美、有效的 UI,同時與開發人員密切配合,設計適當的 ViewModel 來支持其工作。此外,View 與 ViewModel 的分離還使得 ViewModel 更有利於單元測試和重用。

      爲了在 Model、View 和 ViewModel 層之間實施嚴格的分離,我喜歡將每一層構建爲一個單獨的 Visual Studio 項目。與可重用的實用工具、主要的可執行程序集以及任何單元測試項目(您有大量這些內容,對嗎?)結合之後,這會產生大量項目和程序集,如圖 1 所示。

1281333238_ddvip_7147

圖 1 MVVM 應用程序的組成部分

       由於這種嚴格分離的方法會產生大量項目,因此它顯然最適合大型項目。對於只有一兩位開發人員的小型應用程序來說,這種嚴格分離帶來的好處可能無法抵消創建、配置和維護多個項目所帶來的不便,因此僅僅將您的代碼分離到同一個項目的不同命名空間中,可能比充分隔離更好用。

       編寫和維護 ViewModel 並不容易,不應輕率地對待。但是,一些最基本問題(MVVM 設計模式什麼時候是適用的,什麼時候又是不必要的)的答案經常包含在您的域模型中。

      在大型項目中,域模型可能非常複雜,需要精心設計數以百計的類,使它們能夠在任何類型的應用程序(包括 Web 服務、WPF 或 ASP.NET 應用程序)中順暢地結合在一起。Model 可能由幾個相互配合的程序集組成,甚至在超大型組織中,域模型有時是由一個專門的開發團隊構建和維護的。

      如果您有一個複雜的大型域模型,則引入 ViewModel 層幾乎總是會帶來好處。

     另一方面,域模型有時很簡單,可能僅僅是覆蓋在數據庫上的一個薄層。類可以自動生成,而且通常會實現 INotifyPropertyChanged。UI 通常是一系列可供編輯的列表或表格,允許用戶對底層數據進行操作。Microsoft 工具集一直都極其擅長輕鬆快捷地構建這類應用程序。

       如果您的模型或應用程序是這種類型的,則 ViewModel 很可能會帶來難以接受的高開銷,而對您的應用程序設計並沒有足夠的好處。

       儘管如此,即使在這些情況下,ViewModel 也仍然有其價值。例如,ViewModel 非常適合用來實現“撤消”功能。另外,您也可以選擇在應用程序的某個部分(例如文檔管理,我將在後面討論)使用 MVVM 直接向 View 提供 Model。

----------------------------------------------------------------------------------

爲什麼要使用 ViewModel?

       即使 ViewModel 看起來適合您的應用程序,在開始編寫代碼之前,仍然還有問題需要解答。其中最重要的問題是如何減少代理屬性的數量。

       MVVM 設計模式將 View 從 Model 分離,這種做法是該模式的一個重要且有價值的方面。因此,如果 Model 類有 10 個屬性需要在 View 中顯示出來,則 ViewModel 最終通常會有 10 個等效的屬性,這些屬性只是代理了對底層模型實例的調用。這些代理屬性在設置時通常會引發屬性更改事件,通知 View 該屬性已更改。

      並非每個 Model 屬性都要有 ViewModel 代理屬性,但是每個需要在 View 中顯示的 Model 屬性通常都有一個代理屬性。代理屬性通常如下所示:

 
1
2
3
4
5
6
7
8
9
10
11
public string Description
{ 
    get
    { 
        return this.UnderlyingModelInstance.Description; 
    } 
    set
    { 
        this.UnderlyingModelInstance.Description = value; 
        this.RaisePropertyChangedEvent("Description"); 
    } 
}

      任何稍微複雜一點的應用程序都會有數十或上百的 Model 類,這些類都需要按這種方式,通過 ViewModel 向用戶顯示出來。這正是 MVVM 所提供的分離的本質。

編寫這些代理屬性很繁瑣,因此很容易出錯,尤其是在引發屬性更改事件時需要一個字符串,該字符串必須與屬性的名稱相匹配(並且不會包含在任何自動代碼重構中)。爲了消除這些代理事件,常見的解決方法是直接從 ViewModel 包裝器顯示模型實例,然後讓域模型實現 INotifyPropertyChanged 接口:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SomeViewModel
{ 
    public SomeViewModel( DomainObject domainObject )
       { 
        Contract.Requires(domainObject!=null, 
        "The domain object to wrap must not be null"); 
         this.WrappedDomainObject = domainObject; 
    } 
 
public DomainObject WrappedDomainObject
{ 
    get; private set; 
} 
...

      因此,ViewModel 仍然可以提供視圖所需的命令和更多屬性,而無需重複 Model 屬性或創建大量代理屬性。這種方法當然有其吸引力,尤其是在 Model 類已經實現了 INotifyPropertyChanged 接口的情況下。讓模型實現此接口並不一定是壞事,它甚至是 Microsoft .NET Framework 2.0 和 Windows 窗體應用程序中常見的做法。儘管它會使域模型變得很散亂,但確實對 ASP.NET 應用程序或域服務很有用。

    藉助這種方法,View 對 Model 有一定的依賴性,但是這僅僅是通過數據綁定實現的間接依賴,而不需要從 View 項目對 Model 項目進行項目引用。因此,純粹從實用角度出發,此方法有時候很有用。

    但是,此方法實際上違背了 MVVM 設計模式的精神,並且會降低您在將來引入新 ViewModel 功能(例如“撤消”功能)時的能力。我遇到過這種方法導致大量返工的情況。想象一下這種並不罕見的情況:深度嵌套的屬性上有一個數據綁定。如果 Person ViewModel 是當前的數據上下文,且 Person 擁有 Address,則數據綁定可能如下所示:

{Binding WrappedDomainObject.Address.Country}

如果您還需要在 Address 對象上引入更多 ViewModel 功能,您就需要刪除對 WrappedDomainObject.Address 的數據綁定引用,而改爲使用新的 ViewModel 屬性。這就會帶來問題,因爲對 XAML 數據綁定(可能還包括數據上下文)的更新很難進行測試。View 組件沒有自動化的全面迴歸測試。

--------------------------------------------------------------

動態屬性

     我解決代理屬性過多的方法是使用新的 .NET Framework 4 以及支持動態對象和動態方法調度的 WPF。後者使您能夠在運行時決定如何處理類上並不存在的屬性的讀寫操作。這意味着您可以消除 ViewModel 中的所有手寫的代理屬性,同時仍能封裝底層模型。但是請注意,Silverlight 4 不支持綁定到動態屬性。

     實現此功能的最簡單方法是讓 ViewModel 基類擴展新的 System.Dynamic.DynamicObject 類,並重寫 TryGetMember 和 TrySetMember 成員。當被引用的屬性在類上不存在時,動態語言運行時 (DLR) 就會調用這兩個方法,使該類能夠在運行時決定如何實現缺少的屬性。結合少量的反射之後,只需編寫幾行代碼,ViewModel 類就能動態代理對底層模型實例的屬性訪問:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
public override bool TryGetMember
( 
    GetMemberBinder binder, out object result) 
    { 
        string propertyName = binder.Name; 
        PropertyInfo property = 
        this.WrappedDomainObject.GetType().GetProperty(propertyName); 
        if( property==null || property.CanRead==false )
        { 
            result = null; 
            return false; 
        } 
    result = property.GetValue(this.WrappedDomainObject, null);     return true; 
}

       該方法開始時使用反射來查找底層 Model 實例上的屬性。如果該模型沒有這樣一個屬性,該方法將失敗並返回 False,數據綁定也失敗。如果屬性存在,該方法將使用屬性信息來檢索並返回 Model 的屬性值。與傳統代理屬性的 get 方法相比,這是額外的工作,但這也是您需要爲所有模型和所有屬性編寫的唯一實現。

動態代理屬性方法的真正強大之處在於屬性設置器。在 TrySetMember 中,您可以包含常見的邏輯,例如引發屬性更改事件。其代碼如下所示:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
public override bool TrySetMember(SetMemberBinder binder, object value) 
{ 
    string propertyName = binder.Name; 
    PropertyInfo property = 
    this.WrappedDomainObject.GetType().GetProperty(propertyName); 
 
    if( property==null || property.CanWrite==false ) 
        return false; 
 
    property.SetValue(this.WrappedDomainObject, value, null); 
 
    this.RaisePropertyChanged(propertyName); 
    return true; 
}

      同樣,該方法開始時使用反射從底層 Model 實例獲取屬性。如果屬性不存在或是隻讀的,該方法將失敗並返回 False。如果屬性存在於域對象上,將使用屬性信息來設置 Model 屬性。然後您就可以包含對所有屬性設置器均通用的邏輯。在此示例代碼中,我只是爲剛纔設置的屬性引發了屬性更改事件,但您可以輕鬆完成更多任務。

待續.......

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章