談談 INotifyPropertyChanged 的實現


INotifyPropertyChanged 接口是 WPF/Silverlight 開發中非常重要的接口, 它構成了 ViewModel 的基礎, 數據綁定基本上都需要這個接口。 所以, 對它的實現也顯得非常重要, 下面接貼出我知道的幾種實現方式, 希望能起到拋磚引玉的作用。

一般的實現方式

這是一種再普通不過的實現方式, 代碼如下:

public class NotifyPropertyChanged : INotifyPropertyChanged {
   
   public event PropertyChangedEventHandler PropertyChanged;

   virtual internal protected void OnPropertyChanged(string propertyName) {
      if (this.PropertyChanged != null) {
         this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
   }
}

這種方式稱之為一般的實現方式, 因為它確實是太普通不過了, 而且使用起來也讓人感到厭惡, 因為必須指定手工指定屬性名稱:

public class MyViewModel : NotifyPropertyChanged {

   private int _myField;

   public int MyProperty {
      get { return _myField; }
      set {
         _myField = value;
         OnPropertyChanged("MyProperty");
      }
   }
}

lambda 表達式實現方式

對 lambda 表達式比較熟悉的同學可以考慮用 lambda 表達式實現屬性名稱傳遞, 在 NotifyPropertyChanged 類添加一個這樣的方法:

protected void SetProperty<T>(ref T propField, T value, Expression<Func<T>> expr) {
   var bodyExpr = expr.Body as System.Linq.Expressions.MemberExpression;
   if (bodyExpr == null) {
      throw new ArgumentException("Expression must be a MemberExpression!", "expr");
   }
   var propInfo = bodyExpr.Member as PropertyInfo;
   if (propInfo == null) {
      throw new ArgumentException("Expression must be a PropertyExpression!", "expr");
   }
   var propName = propInfo.Name;
   propField = value;
   this.OnPropertyChanged(propName);
}

有了這個方法, NotifyPropertyChanged 基類使用起來就令人舒服了很多:

public class MyViewModel : NotifyPropertyChanged {

   private int _myField;

   public int MyProperty {
      get { return _myField; }
      set {
         base.SetProperty(ref _myField, value, () => this.MyProperty);
          }
   }
}

這樣一來, 把屬性名稱用字符串傳遞改成了用 lambda 表達式傳遞, 減少了硬編碼, 確實方便了不少, 但是還是感覺略微麻煩了一些, 還是要寫一個 lambda 表達式來傳遞屬性名稱。

攔截方式實現

如果對 Castal.DynamicProxy 有印象的話, 可以考慮使用 DynamicProxy 進行攔截實現, 我的實現如下:

// 1. 先定義一個攔截器, 重寫 PostProcess 方法, 當發現是調用以 set_ 開頭的方法時,
//    一般就是設置屬性了, 可以在這里觸發相應的事件。
internal class NotifyPropertyChangedInterceptor : StandardInterceptor {

   protected override void PostProceed(IInvocation invocation) {
      base.PostProceed(invocation);
      var methodName = invocation.Method.Name;
      if (methodName.StartsWith("set_")) {
         var propertyName = methodName.Substring(4);
         var target = invocation.Proxy as NotifyPropertyChanged;
         if (target != null) {
            target.OnPropertyChanged(propertyName);
         }
      }
   }
}

// 2. 再定義一個幫助類, 提供一個工廠方法創建代理類。
public static class ViewModelHelper {

   private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator();
   private static readonly NotifyPropertyChangedInterceptor Interceptor
         = new NotifyPropertyChangedInterceptor();

   public static T CreateProxy<T>(T obj) where T : class, INotifyPropertyChanged {
      return ProxyGenerator.CreateClassProxyWithTarget(obj, Interceptor);
   }
}

使用起來也是很方便的, 只是創建 ViewModel 對象時必須用幫助類來創建實例, 代碼如下:

public class MyViewModel : NotifyPropertyChanged {

   // 定義屬性時不需要任何基類方法, 和普通屬性沒有什么兩樣。
   public int MyProperty {
      get; set;
   }
}
// 使用時需要這樣創建實例:
var viewModel = ViewModelHelper.CreateProxy<MyViewModel>();
viewModel.MyProperty = 100;

不過這種實現的缺點就是所有的屬性都會觸發 PropertyChanged 事件, 而且只能觸發一個事件, 而在實際開發中, 偶爾需要設置一個屬性, 觸發多個 PropertyChanged 事件。

未來 .Net 4.5 的實現方式

在即將發布的 .Net 4.5 中, 提供了 CallerMemberNameAttribute 標記, 利用這個屬性, 可以將上面提供的 SetProperty 方法進行改造, 這樣的實現才是最完美的:

protected void SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null) {
   if (object.Equals(storage, value)) return;

   storage = value;
   this.OnPropertyChanged(propertyName);
}

由於有了 CallerMemberName 標記助陣, 可以說使用起來是非常方便了:

public class MyViewModel : NotifyPropertyChanged {

   private int _myField;

   public int MyProperty {
      get { return _myField; }
      set {
         base.SetProperty(ref _myField, value);
      }
   }
}

這種方法雖然好,不過卻只有在 .Net 4.5 中才有, 而且也許永遠不會添加到 Silverlight 中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM