WPF MVVM之INotifyPropertyChanged接口的几种实现方式
序言
借助WPF/Sliverlight强大的数据绑定功能,可以比实现比MFC,WinForm更加优雅轻松的数据绑定。但是在使用WPF/Silverlight绑定时,有件事情是很苦恼的:当ViewModel对象放生改变,需要通知UI。我们可以让VM对象实现INotifyPropertyChanged接口,通过事件来通知UI。但问题就出现这里……
一,描述问题
情形:现在需要将一个Person对象的Name熟悉双向绑定到UI中的TextBox,的确这是一件很简单的事情,但还是描述下:
XAML:
<TextBox Text="{Binding Name,Mode=TwoWay}"/>
C# Code:
public class Person : INotifyPropertyChanged { private string m_Name; public string Name { get { return m_Name; } set { if (m_Name == value) return; m_Name = value; this.Notify("Name"); } } public Person() { this.m_Name = "墨梅,在这里......"; } public event PropertyChangedEventHandler PropertyChanged; public void Notify(string propertyName) { PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } }
是的,这就可以实现了。但是这里一个问题困惑我,曾经就在this.Notify("Name"),将参数写错,UI迟迟得不到响应。这个错误很难发现!!!也很难跟踪,但是这个细微的错误可以导致一个很严重的运行时错误。这的确是一件很苦恼的事情。
二解决问题
方法一:添加验证
public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { this.VerifyPropertyName(propertyName); PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { var e = new PropertyChangedEventArgs(propertyName); handler(this, e); } } [Conditional("DEBUG")] [DebuggerStepThrough] public void VerifyPropertyName(string propertyName) { // Verify that the property name matches a real, // public, instance property on this object. if (TypeDescriptor.GetProperties(this)[propertyName] == null) { string msg = "Invalid property name: " + propertyName; if (this.ThrowOnInvalidPropertyName) throw new Exception(msg); else Debug.Fail(msg); } }
这里对验证事件参数使用条件编译[Conditional(“DEBUG”)],在release版本中这个函数是不会调用的,比使用#if 等有更明显有优势。
这个方法虽然可以达到目的,但是还是那么的别扭,必须到运行时才能知道是否有错误,所以还是不怎么好。
方法二,使用Lambda表达式,静态扩展语法
public static class NotificationExtensions { public static void Notify(this PropertyChangedEventHandler eventHandler, Expression<Func<object>> expression) { if( null == eventHandler ) { return; } var lambda = expression as LambdaExpression; MemberExpression memberExpression; if (lambda.Body is UnaryExpression) { var unaryExpression = lambda.Body as UnaryExpression; memberExpression = unaryExpression.Operand as MemberExpression; } else { memberExpression = lambda.Body as MemberExpression; } var constantExpression = memberExpression.Expression as ConstantExpression; var propertyInfo = memberExpression.Member as PropertyInfo; foreach (var del in eventHandler.GetInvocationList()) { del.DynamicInvoke(new object[] {constantExpression.Value, new PropertyChangedEventArgs(propertyInfo.Name)}); } } }
这里用使用的静态扩展语法,我还是比较喜欢这个的,但是并不是所有人都喜欢哦。如何使用呢:
public class Employee : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _firstName; public string FirstName { get { return this._firstName; } set { this._firstName = value; this.PropertyChanged.Notify(()=>this.FirstName); } } }
这里还可以添加一个很实用的扩展:
public static void SubscribeToChange<T>(this T objectThatNotifies, Expression<Func<object>> expression, PropertyChangedEventHandler<T> handler) where T : INotifyPropertyChanged { objectThatNotifies.PropertyChanged += (s, e) => { var lambda = expression as LambdaExpression; MemberExpression memberExpression; if (lambda.Body is UnaryExpression) { var unaryExpression = lambda.Body as UnaryExpression; memberExpression = unaryExpression.Operand as MemberExpression; } else { memberExpression = lambda.Body as MemberExpression; } var propertyInfo = memberExpression.Member as PropertyInfo; if(e.PropertyName.Equals(propertyInfo.Name)) { handler(objectThatNotifies); } }; }
通过上面的代码,可以订阅熟悉改变事件,如:
myObject.SubscripeToChange(()=>myObject.SomeProperty,SomeProperty_Changed); And then your handler would look like this: private void SomeProperty_Changed(MyObject myObject) { /* ... implement something here */ }
方法三,net4.5,框架提供的解决方法
private string m_myProperty; public string MyProperty { get { return m_myProperty; } set { m_myProperty = value; OnPropertyChanged(); } } private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed") { // ... do stuff here ... }
属性CallerMemberName的解决办法和方法二是基本相同的,不同的是这个在net框架中解决的。更多信息可以查看CallerMemberName,net4.5还提供了
CallerFilePath,CallerLineNumber,这几很有用的语法
方法四,这个也不错哦
public static class SymbolExtensions { public static string GetPropertySymbol<T,R>(this T obj, Expression<Func<T,R>> expr) { return ((MemberExpression)expr.Body).Member.Name; } } public class ConversionOptions : INotifyPropertyChanged { private string _outputPath; public string OutputPath { get { return _outputPath;} set { _outputPath = value; OnPropertyChanged(o => o.OutputPath); } } private string _blogName; public string BlogName { get { return _blogName;} set { _blogName = value; OnPropertyChanged(o => o.BlogName); } } private string _secretWord; public string SecretWord { get { return _secretWord; } set { _secretWord = value; OnPropertyChanged(o => o.SecretWord); } } protected virtual void OnPropertyChanged<R>(Expression<Func<ConversionOptions, R>> propertyExpr) { OnPropertyChanged(this.GetPropertySymbol(propertyExpr)); } protected virtual void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged; }
注释:这里还有更多参考信息,您可以在这里了解更加清楚: