天天看點

如何利用AOP簡化MVVM中Model和ViewModel的設計

這一篇談一個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類型。

  1. 為了實作雙向綁定,并且在屬性發生變化時接收通知,我們通常需要實作一個接口,叫INotifyPropertyChanged。
  2. 這個接口隻有一個事件(PropertyChanged)。通常為了觸發該事件,我們會定義一個統一的方法(OnPropertyChanged)
  3. 通常在每個屬性中的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,我們的問題就很容易可以解決了。請大家下載下傳,安裝,然後在項目中添加兩個引用

如何利用AOP簡化MVVM中Model和ViewModel的設計

我們可以編寫下面一個特殊的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對象,并且對它兩個屬性都做了更改。

繼續閱讀