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 中。