[WPF 基础知识系列] —— 更优雅地实现 INotifyPropertyChanged

时间:2021-11-25 13:12:17

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:

[WPF 基础知识系列] —— 更优雅地实现 INotifyPropertyChanged[WPF 基础知识系列] —— 更优雅地实现 INotifyPropertyChanged
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));
            }
        }
    }
}
ObservableObject Final Version 1

 最终不修改版2:

[WPF 基础知识系列] —— 更优雅地实现 INotifyPropertyChanged[WPF 基础知识系列] —— 更优雅地实现 INotifyPropertyChanged
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));
            }
        }
    }
}
ObservableObject Final Version 2

 

  

 

代码基本上简洁到不能再简洁了,唯一的遗憾是ref,但是为了支持值类型,这个是没有办法的事情。

好的,到此为止大功告成。

如果大家有更好的方法,可以随意提出来,我们交流改进。