INotifyPropertyChanged 是WPF中非常重要的一个概念,它也是实现Binding 肯定要用到的一个接口(对于非Dependency Property而言)。
注:以下内容都是对普通对象而言的,Dependency Property有自己的通知机制,不需要额外去Notify。
因为它能够提供一个通知的功能,当我们修改了某个source对象后,就需要通知target,让target知道source修改了,然后进行update。
本文不是要讲INotifyPropertyChanged 具体如何使用,所以这里也就不贴代码展示了,如果你还不太清楚它的用途,
那么可以先看一下这篇博客:玩转INotifyPropertyChanged和ObservableCollection。
本文,主要是想要介绍一下如何更加方便、优雅、简洁的来使用INotifyPropertyChanged。
因为我们要很频繁的使用INotifyPropertyChanged,特别是当写Model的对象时,这时就会有很多问题出现。
下面先介绍一下,最普通的Model对象。
// 版本1
public class Person : INotifyPropertyChanged { private string name; public string Name { get { return name; } set { name = value; this.OnPropertyChanged("Name"); } }
#region implement property changed public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } }
#endregion }
从上面这个类看来,有几个可以改进的地方:
1、如果我们每次创建一个Model类,那么我们就会重复很多代码(region区域内的代码),可以提取到一个另外的类中去;
虽然,有时候必须使用接口(因为不允许继承多个类),但是对于Model类,一般不需要继承其他类,所以可以提取一个类出来。
2、每次我们创建一个属性时,就要在Set中OnPropertyChanged一下,而且这里使用一个字符串来代表名字,
这样做不仅很麻烦,而且有时会造成很难察觉的错误(单词拼错了,造成Notify无效)。
根据上面的分析,我们可以修改一下这个Model:
1、提取到一个基类NotifyPropertyChangedEx中:
// 版本2
public abstact class NotifyPropertyChangedEx:INotifyPropertyChanged
{ #region Notify Property Changed public event PropertyChangedEventHandler PropertyChanged; public void NotifyPropertyChanged<T>(Expression<Func<T>> propertyName) { if (this.PropertyChanged != null) { var memberExpression = propertyName.Body as MemberExpression; if (memberExpression != null) { OnPropertyChanged(memberExpression.Member.Name); } } } #endregion }
2、使用时,使用Lambda表达式代替之前的字符串传入属性名:
public class Person : NotifyPropertyChangedEx { private string name; public string Name { get { return name; } set { name = value; this.NotifyPropertyChanged(() => Name); } } }
现在来看,是不是简洁了不少,也优雅了不少呀!
但是对于一个程序猿来讲,这样还是不够完美。因为每次都要重复的写this.NotifyPropertyChanged(...);
所以,我们可以在进行一下修改。
我们再来根据我们的要求,再修改一次。
1、将Notify方法提取到父类的一个SetProperty方法中:
// 版本3 public abstract class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void SetProperty<T>(ref T field, T value, Expression<Func<T>> expr) { if (!EqualityComparer<T>.Default.Equals(field, value)) { field = value; var lambda = (LambdaExpression)expr; MemberExpression memberExpr; if (lambda.Body is UnaryExpression) { var unaryExpr = (UnaryExpression)lambda.Body; memberExpr = (MemberExpression)unaryExpr.Operand; } else { memberExpr = (MemberExpression)lambda.Body; } var handler = this.PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(memberExpr.Member.Name)); } } } }
2、在子类中,使用SetProperty方法来操作:
public class Person : NotifyPropertyChangedEx { private string name; public string Name { get { return name; } set { this.SetProperty(ref name, value, ()=>Name); } } }
现在来看,是不是更加简洁了。
当然,如果你使用C#5.0中的新特性CallerMemberName的话,将更加简单。因为连这个Lambda表达式也不再需要了。
下面,我来就要写一个基于CallerMemberName的例子吧。
1、使用[CallerMemberName]来替代Lambda表达式:
//C#5.0 版本
public abstract class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged;
protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null) { if (EqualityComparer<T>.Default.Equals(field, value)) { return; } field = value; var handler = this.PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } }
2、现在,我们再来看一下,如何使用:
public class Person : NotifyPropertyChangedEx { private string name; public string Name { get { return name; } set { this.SetProperty(ref name, value); } } }
最终不修改版1:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq.Expressions; namespace Contacts.Infrastructure { public class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged = delegate { }; public void SetProperty<TProperty>(ref TProperty field, TProperty value, Expression<Func<TProperty>> property) { if (!EqualityComparer<TProperty>.Default.Equals(field, value)) { field = value; this.NotifyOfPropertyChange(property); } } /// <summary> /// Notifies subscribers of the property change. /// </summary> /// <param name="propertyName">Name of the property.</param> public virtual void NotifyOfPropertyChange(string propertyName) { this.RaisePropertyChangedEventCore(propertyName); } /// <summary> /// Notifies subscribers of the property change. /// </summary> /// <typeparam name="TProperty">The type of the property.</typeparam> /// <param name="property">The property expression.</param> public virtual void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property) { var lambda = (LambdaExpression)property; MemberExpression memberExpression; var body = lambda.Body as UnaryExpression; if (body != null) { var unaryExpression = body; memberExpression = (MemberExpression)unaryExpression.Operand; } else { memberExpression = (MemberExpression)lambda.Body; } this.NotifyOfPropertyChange(memberExpression.Member.Name); } private void RaisePropertyChangedEventCore(string propertyName) { var handler = this.PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } } }
最终不修改版2:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq.Expressions; using System.Runtime.CompilerServices; using DrawdingBoard1.Annotations; namespace DrawdingBoard1.Common { public class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// Notifies subscribers of the property change. /// </summary> /// <param name="propertyName">Name of the property, can be auto detected.</param> [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { RaisePropertyChangedEventCore(propertyName); } /// <summary> /// Notifies subscribers of the property change. /// </summary> /// <param name="propertyName">Name of the property.</param> public virtual void NotifyPropertyChanged(string propertyName) { RaisePropertyChangedEventCore(propertyName); } /// <summary> /// Notifies subscribers of the property change. /// </summary> /// <typeparam name="TProperty">The type of the property.</typeparam> /// <param name="property">The property expression.</param> public virtual void NotifyPropertyChanged<TProperty>(Expression<Func<TProperty>> property) { var lambda = (LambdaExpression) property; MemberExpression memberExpression; var body = lambda.Body as UnaryExpression; if (body != null) { var unaryExpression = body; memberExpression = (MemberExpression) unaryExpression.Operand; } else { memberExpression = (MemberExpression) lambda.Body; } NotifyPropertyChanged(memberExpression.Member.Name); } /// <summary> /// A set method which can notifies subscribers of the property change. /// </summary> /// <typeparam name="TProperty">The type of this property.</typeparam> /// <param name="field">The property.</param> /// <param name="value">The target value.</param> /// <param name="property">The property expression.</param> public void SetProperty<TProperty>(ref TProperty field, TProperty value, Expression<Func<TProperty>> property) { if (!EqualityComparer<TProperty>.Default.Equals(field, value)) { field = value; NotifyPropertyChanged(property); } } /// <summary> /// A set method which can notifies subscribers of the property change. /// </summary> /// <typeparam name="TProperty">The type of this property.</typeparam> /// <param name="field">The property.</param> /// <param name="value">The target value.</param> /// <param name="propertyName">Name of the property, can be auto detected.</param> public void SetProperty<TProperty>(ref TProperty field, TProperty value, [CallerMemberName] string propertyName = null) { if (!EqualityComparer<TProperty>.Default.Equals(field, value)) { field = value; RaisePropertyChangedEventCore(propertyName); } } /// <summary> /// Core method of raise property changed. /// </summary> /// <param name="propertyName">The name of property that will be notified.</param> private void RaisePropertyChangedEventCore(string propertyName) { var handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } } }
代码基本上简洁到不能再简洁了,唯一的遗憾是ref,但是为了支持值类型,这个是没有办法的事情。
好的,到此为止大功告成。
如果大家有更好的方法,可以随意提出来,我们交流改进。