INotifyPropertyChanged 及其他 - 第一部分
改进和扩展 INotifyPropertyChanged 接口。

引言
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
}
总结
我们已经开始构建基础代码,它将允许我们的应用程序更精细地控制我们对象的变化。这些事件在使用不允许类似控制的第三方控件时非常有用。
演示应用程序包含了本文所有完成的代码和一个简单的示例,展示了它的实际应用。
在本文的下一部分,我们将解决以下改进
- 传播支持 – 当对象以分层方式组织时(例如,父/子),这允许对象将其 `PropertyChanged` / `PropertyChanging` 事件传播给其父级。这允许客户端应用程序挂接一个事件处理程序来接收任何更改通知。
- 批量更改支持 – 当更新大量属性时,可能希望将这些更改分组为单个事件。在这种情况下,我们希望知道所有已更改的属性以及它们如何更改。
- 事件抑制 – 有时我们可能希望完全抑制这些事件。因此,我们将添加对关闭事件的支持。
历史
- 2007 年 5 月 7 日:初次发布