65.9K
CodeProject 正在变化。 阅读更多。
Home

INotifyPropertyChanged 及其他 - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (21投票s)

2007年5月7日

CPOL

4分钟阅读

viewsIcon

73229

downloadIcon

961

改进和扩展 INotifyPropertyChanged 接口。

Screenshot - screenshot.png

引言

INotifyPropertyChanged 接口为对象提供了一个标准事件,用于通知客户端其某个属性已更改。这对于数据绑定(如本文 文章中所述)非常有用,因为它允许绑定的控件根据直接更改底层对象来更新其显示。虽然此事件达到了其目的,但仍有改进的空间。

本文将介绍对此接口的一些改进和扩展。我们将从简化 `INotifyPropertyChanged` 的使用开始,并逐步添加新功能。我们将使用一个名为 `IPropertyNotification` 的新接口来扩展 `INotifyPropertyChanged` 接口。

IPropertyNotification 接口和基类

首先,`IPropertyNotification` 接口将简单定义为

public interface IPropertyNotification : INotifyPropertyChanged {
    // No members
}

然后,我们将定义一个基类,如下所示,它将允许我们的派生对象轻松地利用此接口

/// <summary>
/// This class implements the <see cref="T:IPropertyNotification"/>
/// interface and provides helper methods for derived classes.
/// </summary>
public class PropertyNotificationObject : IPropertyNotification {
    #region IPropertyNotification

    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    [field:NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;

    #endregion // IPropertyNotification

    #region Methods

    /// <summary>
    /// Raises the <see cref="E:PropertyChanged"/> event.
    /// </summary>
    /// <param name="propertyName">
    /// Name of the property that changed.
    /// </param>
    protected void OnPropertyChanged(String propertyName) {
        PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
        OnPropertyChanged(e);
    }

    /// <summary>
    /// Raises the <see cref="E:PropertyChanged"/> event.
    /// </summary>
    /// <param name="e">
    /// The <see cref="PropertyChangedEventArgs"/> instance
    /// containing the event data.
    /// </param>
    protected void OnPropertyChanged(PropertyChangedEventArgs e) {
        PropertyChangedEventHandler temp = this.PropertyChanged;
        if (null != temp)
            temp(this, e);
    }

    #endregion // Methods
}

正如您所看到的,基类允许派生类轻松地调用 `PropertyChanged` 事件。如下图所示

/// <summary>
/// This class is used to test the functionality of
/// <see cref="T:PropertyNotificationObject"/> and
/// <see cref="T:IPropertyNotification"/>.
/// </summary>
public class TestObject : PropertyNotificationObject {
    #region Properties

    /// <summary>
    /// Holds the name.
    /// </summary>
    private String name = String.Empty;

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>The name.</value>
    public String Name {
        get {
            return this.name;
        }
        set {
            if (false == Object.Equals(value, this.name)) {
                this.name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    #endregion // Properties
}

现在我们已经定义了基类和接口,我们将开始添加一些新功能。

有什么变化?

当触发 `PropertyChanged` 事件时,只会提供更改属性的名称。在某些情况下,了解属性的先前值和新值会很有用。可以使用事件中当前提供的信息获取新值。具体来说,可以对发送者(假设发送者是更改的对象)使用反射来获取以事件指定的名称命名的属性的值。但这往往会变得混乱,并且不允许我们获取先前的值。

与其使用反射,不如实现一个派生自 `PropertyChangedEventArgs` 的类,该类将携带旧值和新值。此类如下所示

/// <summary>
/// This class extends <see cref="T:PropertyChangedEventArgs"/> and
/// allows for storing the old and new values of the changed property.
/// </summary>
public class PropertyNotificationEventArgs : PropertyChangedEventArgs {
    #region Constructors

    /// <summary>
    /// Initializes a new instance of the
    /// <see cref="PropertyNotificationEventArgs"/> class.
    /// </summary>
    /// <param name="propertyName">
    /// The name of the property that is associated with this
    /// notification.
    /// </param>
    public PropertyNotificationEventArgs(String propertyName)
        : this(propertyName, null, null) {
        // No-op
    }

    /// <summary>
    /// Initializes a new instance of the
    /// <see cref="PropertyNotificationEventArgs"/> class.
    /// </summary>
    /// <param name="propertyName">
    /// The name of the property that is associated with this
    /// notification.
    /// </param>
    /// <param name="oldValue">The old value.</param>
    /// <param name="newValue">The new value.</param>
    public PropertyNotificationEventArgs(String propertyName,
        Object oldValue, Object newValue)
        : base(propertyName) {
        this.oldValue = oldValue;
        this.newValue = newValue;
    }

    #endregion // Constructors

    #region Properties

    /// <summary>
    /// Holds the new value of the property.
    /// </summary>
    private Object newValue;

    /// <summary>
    /// Gets the new value of the property.
    /// </summary>
    /// <value>The new value.</value>
    public Object NewValue {
        get {
            return this.newValue;
        }
    }

    /// <summary>
    /// Holds the old value of the property.
    /// </summary>
    private Object oldValue;

    /// <summary>
    /// Gets the old value of the property.
    /// </summary>
    /// <value>The old value.</value>
    public Object OldValue {
        get {
            return this.oldValue;
        }
    }

    #endregion // Properties
}

为了使用这个新类,我们必须修改我们的基类和测试对象,如下所示

/// <summary>
/// This class implements the <see cref="T:IPropertyNotification"/>
/// interface and provides helper methods for derived classes.
/// </summary>
public class PropertyNotificationObject : IPropertyNotification {
    // ... Existing code ...

    #region Methods

    /// <summary>
    /// Raises the <see cref="E:PropertyChanged"/> event.
    /// </summary>
    /// <param name="propertyName">
    /// Name of the property that changed.
    /// </param>
    /// <param name="oldValue">The old value.</param>
    /// <param name="newValue">The new value.</param>
    protected void OnPropertyChanged(String propertyName,
        Object oldValue, Object newValue) {
        PropertyNotificationEventArgs e = 
	new PropertyNotificationEventArgs(propertyName,
            oldValue, newValue); // ** Pass in the old and new value
        OnPropertyChanged(e);
    }

    // ... Existing code ...

    #endregion // Methods
}

/// <summary>
/// This class is used to test the functionality of
/// <see cref="T:PropertyNotificationObject"/> and
/// <see cref="T:IPropertyNotification"/>.
/// </summary>
public class TestObject : PropertyNotificationObject {
    #region Properties

    // ... Existing code ...

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>The name.</value>
    public String Name {
        // ... Existing code ...
        set {
            if (false == Object.Equals(value, this.name)) {
                String oldValue = this.name; // ** Save the old value
                this.name = value;
	       // ** Pass in the old and new value
                OnPropertyChanged("Name", oldValue, this.name); 
            }
        }
    }

    #endregion // Properties
}

这种方法的一个问题是,客户端必须使用“is”或“as”运算符来确定给定的 `PropertyChangedEventArgs` 是否确实是 `PropertyNotificationEventArgs` 的实例。我们可以通过在 `IPropertyNotification` 接口中创建一个新的事件,该事件接受 `PropertyNotificationEventArgs` 的实例来解决这个问题,但这样我们就失去了与已支持 `INotifyPropertyChanged` 的客户端的无缝集成。

取消更改

在许多情况下,仅在属性更改后收到事件就足够了。在许多情况下,也需要一个在属性更改之前可以取消的事件(例如,源代码控制、验证等)。为了更好地支持这些类型的场景,我们将向接口添加一个 `PropertyChanging` 事件。首先,我们需要定义一个派生自 `PropertyNotificationEventArgs` 的类,该类允许我们取消事件。此类如下所示

/// <summary>
/// This class extends <see cref="T:PropertyNotificationEventArgs"/> and
/// allows for cancelling of the associated event.
/// </summary>
public class CancelPropertyNotificationEventArgs : PropertyNotificationEventArgs {
    #region Constructors

    /// <summary>
    /// Initializes a new instance of the
    /// <see cref="CancelPropertyNotificationEventArgs"/> class.
    /// </summary>
    /// <param name="propertyName">
    /// The name of the property that is associated with this
    /// notification.
    /// </param>
    public CancelPropertyNotificationEventArgs(String propertyName)
        : base(propertyName) {
        // No-op
    }

    /// <summary>
    /// Initializes a new instance of the
    /// <see cref="CancelPropertyNotificationEventArgs"/> class.
    /// </summary>
    /// <param name="propertyName">
    /// The name of the property that is associated with this
    /// notification.
    /// </param>
    /// <param name="oldValue">The old value.</param>
    /// <param name="newValue">The new value.</param>
    public CancelPropertyNotificationEventArgs(String propertyName,
        Object oldValue, Object newValue)
        : base(propertyName, oldValue, newValue) {
        // No-op
    }

    #endregion // Constructors

    #region Properties

    /// <summary>
    /// Holds a value indicating whether the associated event should be
    /// cancelled.
    /// </summary>
    private Boolean cancel = false;

    /// <summary>
    /// Gets or sets a value indicating whether the associated event should
    /// be cancelled.
    /// </summary>
    /// <value>
    /// <c>true</c> if the event should be cancelled; otherwise, <c>false</c>.
    /// </value>
    public Boolean Cancel {
        get {
            return this.cancel;
        }
        set {
            this.cancel = value;
        }
    }

    #endregion // Properties
}

接下来,我们将向接口添加一个新事件,并向基类添加相关的辅助方法,如下所示

/// <summary>
/// Notifies clients that a property value is changing or changed.
/// </summary>
public interface IPropertyNotification : INotifyPropertyChanged {
    #region Events

    /// <summary>
    /// Occurs when a property value is changing.
    /// </summary>
    event PropertyChangingEventHandler PropertyChanging;

    #endregion // Events
}
/// <summary>
/// This class implements the <see cref="T:IPropertyNotification"/>
/// interface and provides helper methods for derived classes.
/// </summary>
public class PropertyNotificationObject : IPropertyNotification {
    #region IPropertyNotification

    // ... Existing code ...

    /// <summary>
    /// Occurs when a property value is changing.
    /// </summary>
    [field: NonSerialized]
    public event PropertyChangingEventHandler PropertyChanging;

    #endregion // IPropertyNotification

    #region Methods

    // ... Existing code ...

    /// <summary>
    /// Raises the <see cref="E:PropertyChanging"/> event.
    /// </summary>
    /// <param name="propertyName">
    /// Name of the property that is changing.
    /// </param>
    /// <param name="oldValue">The old value.</param>
    /// <param name="newValue">The new value.</param>
    /// <returns><c>true</c> if the change can continue; 
    /// otherwise <c>false</c>.</returns>
    protected Boolean OnPropertyChanging(String propertyName,
        Object oldValue, Object newValue) {
        CancelPropertyNotificationEventArgs e = 
			new CancelPropertyNotificationEventArgs(propertyName,
            oldValue, newValue);
        OnPropertyChanging(e);
        return !e.Cancel;
    }

    /// <summary>
    /// Raises the <see cref="E:PropertyChanging"/> event.
    /// </summary>
    /// <param name="e">
    /// The <see cref="CancelPropertyNotificationEventArgs"/> instance
    /// containing the event data.
    /// </param>
    protected void OnPropertyChanging(CancelPropertyNotificationEventArgs e) {
        PropertyChangingEventHandler temp = this.PropertyChanging;
        if (null != temp)
            temp(this, e);
    }

    #endregion // Methods
}

最后,我们可以将新事件连接到我们的测试对象,如下所示

/// <summary>
/// This class is used to test the functionality of
/// <see cref="T:PropertyNotificationObject"/> and
/// <see cref="T:IPropertyNotification"/>.
/// </summary>
public class TestObject : PropertyNotificationObject {
    #region Properties

    // ... Existing code ...

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>The name.</value>
    public String Name {
        // ... Existing code ...
        set {
            if (false == Object.Equals(value, this.name)) {
                if (true == OnPropertyChanging("Name", this.name, value)) 
	        { // ** Call Changing event
                    String oldValue = this.name;
                    this.name = value;
                    OnPropertyChanged("Name", oldValue, this.name);
                }
            }
        }
    }

    #endregion // Properties
}

使用这个新方法,现在可以取消 `TestObject` 实例上的更改。这包括来自 `PropertyGrid`、数据绑定控件的更改,或直接访问的更改。

模板化 Set 代码

我们已将 `Property` set 方法中的代码提取到基类中的一个辅助方法中。虽然这可能处理大多数情况,但仍会有此辅助方法不起作用的情况。新辅助方法如下所示

/// <summary>
/// This method is used to set a property while firing associated
/// PropertyChanging and PropertyChanged events.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
/// <param name="propertyField">The property field.</param>
/// <param name="value">The value.</param>
protected void SetProperty<T>(String propertyName, ref T propertyField,
    T value) {
    if (false == Object.Equals(value, propertyField)) {
        if (true == OnPropertyChanging(propertyName, propertyField, value)) {
            T oldValue = propertyField;
            propertyField = value;
            OnPropertyChanged(propertyName, oldValue, propertyField);
        }
    }
}

并且 `TestObject` 是这样使用它的

/// <summary>
/// This class is used to test the functionality of
/// <see cref="T:PropertyNotificationObject"/> and
/// <see cref="T:IPropertyNotification"/>.
/// </summary>
public class TestObject : PropertyNotificationObject {
    #region Properties

    // ... Existing code ...

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>The name.</value>
    public String Name {
        // ... Existing code ...
        set {
            SetProperty<String>("Name", ref this.name, value);
        }
    }

    #endregion // Properties
}

总结

我们已经开始构建基础代码,它将允许我们的应用程序更精细地控制我们对象的变化。这些事件在使用不允许类似控制的第三方控件时非常有用。

演示应用程序包含了本文所有完成的代码和一个简单的示例,展示了它的实际应用。

在本文的下一部分,我们将解决以下改进

  1. 传播支持 – 当对象以分层方式组织时(例如,父/子),这允许对象将其 `PropertyChanged` / `PropertyChanging` 事件传播给其父级。这允许客户端应用程序挂接一个事件处理程序来接收任何更改通知。
  2. 批量更改支持 – 当更新大量属性时,可能希望将这些更改分组为单个事件。在这种情况下,我们希望知道所有已更改的属性以及它们如何更改。
  3. 事件抑制 – 有时我们可能希望完全抑制这些事件。因此,我们将添加对关闭事件的支持。

历史

  • 2007 年 5 月 7 日:初次发布
© . All rights reserved.