這一篇談一個MVVM中的一個雖然小但卻很實際的問題,就是如何簡化Model和ViewModel的設計。這是我們在項目中總結提煉的一些做法。
【備注】關于MVVM的概念,并不是本文的重點。如果你對MVVM還不熟悉,可以參考這裡。關于MVVM與之前的MVP,MVC設計模式的淵源和比較,還有目前主流的幾個MVVM架構的大緻情況,我最近可能再會抽時間另外整理一篇,有興趣的朋友關注一下。
有用過MVVM的朋友,都知道我們在項目中需要定義Model和ViewModel。Model指的是資料實體,它負責存儲資料,并且提供了與外部資源(例如資料庫或者遠端服務)的互動。ViewModel是指View與Model之間的一個橋梁,通常情況下,View是指界面(例如WPF中的Window,或者Silverlight中的Page等等),使用MVVM的核心目的是讓View的設計能夠更加獨立,它不應該包含太多的資料邏輯。極端情況下,它不應該有一行自定義代碼。那麼,你可能會問,不用代碼怎麼顯示和更新資料呢?答案就是,View通過綁定(Binding)連接配接到ViewModel,在WPF和Silverlight中都可以實作雙向(TwoWay)的綁定。這樣就能實作資料的顯示和更新。同時,ViewModel中還可以公開一些Command,以便可以讓View中的特殊操作可以綁定。——這就是MVVM的核心理論。
那麼,言歸正傳吧,在介紹我們的問題之前,我先給大家看一個典型的Model類型
using System.Diagnostics;
using System.ComponentModel;
namespace WPFMVVMSample.Models
{
public class Employee:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
//為了便于調試,我們在Output視窗輸出一行資訊
Debug.WriteLine(string.Format("{0} Changed", name));
}
private string firstName = string.Empty;
public string FirstName
{
get { return firstName; }
set {
if (value != firstName)
{
firstName = value;
OnPropertyChanged("FirstName");
}
}
}
private string lastName = string.Empty;
public string LastName
{
get { return lastName; }
set
{
if (value != lastName)
{
lastName = value;
OnPropertyChanged("LastName");
}
}
}
private int age = 18;
public int Age
{
get { return age; }
set {
if (value != age)
{
age = value;
OnPropertyChanged("Age");
}
}
}
}
}
是不是很熟悉這個代碼呢?這是一個表示員工資訊的Model類型。
- 為了實作雙向綁定,并且在屬性發生變化時接收通知,我們通常需要實作一個接口,叫INotifyPropertyChanged。
- 這個接口隻有一個事件(PropertyChanged)。通常為了觸發該事件,我們會定義一個統一的方法(OnPropertyChanged)
- 通常在每個屬性中的set方法器中,我們要去調用OnPropertyChanged,發出屬性已更改的通知。
也不算複雜對吧,但問題在于,如果這個Model類型有很多屬性的話,那麼這個類就會變得很冗長。而且很多代碼其實都是同樣的寫法。更何況,一個項目裡面可能會有很多個Model類型呢?
我們的問題就是:有沒有什麼方式來讓自動完成這樣的工作呢?也就是說,每個屬性的Set方法執行完之後,自動地調用OnPropertyChanged這個方法。
例如,我們能不能還是按照下面這樣定義Model類型呢?
namespace WPFMVVMSample.Models
{
public class Customer
{
public string CustomerID { get; set; }
public string CompanyName { get; set; }
}
}
我想,一個Model類型本應就這麼簡單,你不這麼覺得嗎?那麼,就随我一步一步來做個實驗吧
首先,考慮到可能有很多個Model類型,每個類型都去實作那個INotifyPropertyChanged接口,就顯得不是那麼理想。針對這個問題,我們很自然地想到将這部分實作提取到一個基類去。我們确實是這麼做的。例如,下面這裡我們定義一個ModelBase類型
using System.ComponentModel;
using System.Diagnostics;
namespace WPFMVVMSample.Models
{
public abstract class ModelBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
//為了便于調試,我們在Output視窗輸出一行資訊
Debug.WriteLine(string.Format("{0} Changed", name));
}
}
}
那麼,接下來,我們就要讓Customer類型繼承ModelBase
namespace WPFMVVMSample.Models
{
public class Customer:ModelBase
{
public string CustomerID { get; set; }
public string CompanyName { get; set; }
}
}
我們已經簡化了實作接口的那部分工作。但是,還有一個關鍵點,如何讓每個屬性的set方法器中自動調用基類中定義好的OnPropertyChanged方法呢?也就是說,我們希望通過一個什麼樣的方式在每個set方法後面插入一個特殊的代碼邏輯。是不是這樣呢?
我聯想到以前用過的一個所謂的AOP(面向方面的程式設計)的架構,當初還寫過一篇文章介紹,請參考下面的連結
PostSharp的AOP設計在.NET Remoting中的應用
我曾經用過一個業界比較認可的靜态AOP架構,叫做Postsharp。它的官方網站在下面
http://www.sharpcrafters.com/
有了Postsharp,我們的問題就很容易可以解決了。請大家下載下傳,安裝,然後在項目中添加兩個引用

我們可以編寫下面一個特殊的Attribute
using System;
using PostSharp.Laos;
namespace WPFMVVMSample
{
/// <summary>
/// 陳希章
/// 2011-6-24
/// 這是一個特殊的Attribute,是postsharp中實作方法注入的一個做法
/// </summary>
[Serializable]
[AttributeUsage(AttributeTargets.Class,Inherited=true,AllowMultiple=false)]
public class NotifyPropertyChangeAttribute:OnMethodBoundaryAspect
{
public override void OnSuccess(MethodExecutionEventArgs eventArgs)
{
var methodName = eventArgs.Method.Name;
var type = eventArgs.Instance.GetType();
var targetMethod= type.GetMethod("OnPropertyChanged",
System.Reflection.BindingFlags.NonPublic| System.Reflection.BindingFlags.Instance);
if (methodName.StartsWith("set_") && targetMethod != null)//隻針對這種方法器進行注入
{
var propertyName = methodName.Substring(4);//解析得到屬性名稱
targetMethod.Invoke(eventArgs.Instance, new[] { propertyName });//執行該方法
}
}
}
}
那麼,如何實作這個特殊的Attribute呢?我們隻要在Customer這個類型上添加它就可以了。最終設計好的Customer類型如下
namespace WPFMVVMSample.Models
{
[NotifyPropertyChange]
public class Customer:ModelBase
{
public string CustomerID { get; set; }
public string CompanyName { get; set; }
}
}
大家可以将這個類型與本文最開頭的Employee類型比較一下,代碼明顯精簡了很多很多。
至于ViewModel,也是同樣的做法即可。
你可能會疑惑地說,真有這麼神奇嗎?上面這樣做了之後,有沒有真正地生效呢?
為了讓你看到效果,又不免很複雜,還記得我們在ModelBase裡面有下面一句代碼嗎?
//為了便于調試,我們在Output視窗輸出一行資訊
Debug.WriteLine(string.Format("{0} Changed", name));
也就是說,隻要屬性發生了變化,就發出通知,并且在Output視窗中顯示一些資訊。
我們可以做一個簡單的測試,在MainWindow中加入如下的代碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WPFMVVMSample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var customer = new Models.Customer();
customer.CustomerID = "microsoft";
customer.CompanyName = "microsoft company";
this.DataContext = customer;
}
}
}
我們這裡初始化了一個Customer對象,并且對它兩個屬性都做了更改。