使用 WatchableObject 实现 INotifyPropertyChanged 接口。





0/5 (0投票)
介绍 WatchableObject,这是一个用于实现 INotifyPropertyChanged 接口的基类。
此类已弃用。请检查 PropertyObservable 类。
引言
如果您是使用 MVVM 模式的 WPF 开发人员,您可能知道 INotifyPropertyChanged 接口的重要性。INotifyPropertyChanged 的实现会影响视图模型中几乎所有的属性。因此,对 INotifyPropertyChanged 实现的任何微小改进都可能有效地提高整体编码生产力。WatchableObject 是为此目的而编写的纯 C# 类。
基础
INotifyPropertyChanged 接口的定义如下:
// Notifies clients that a property value has changed.
public interface INotifyPropertyChanged
{
// Occurs when a property value changes.
event PropertyChangedEventHandler PropertyChanged;
}
因此,如果您从头开始实现它,您需要像这样编写 PropertyChanged 事件:
public partial class Test : INotifyPropertyChanged
{
// Occurs when a property value is changed.
public event PropertyChangedEventHandler PropertyChanged;
// Raises a PropertyChanged event.
// An empty or null property name indicates that all of the properties have changed.
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged != null)
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
并且您必须为每个您想要通知更改的属性更改引发事件。以下显示了一个名为 Number 的支持更改通知的属性:
public partial class Test : INotifyPropertyChanged
{
int _number;
public int Number
{
get { return _number; }
set
{
if (_number == value)
return;
_number = value;
OnPropertyChanged("Number");
}
}
}
如果添加了只读计算属性,则必须在它所依赖的属性更改时引发事件。例如,此代码声明了两个计算属性:IsOddNumber、IsEvenNumber。这两个属性都依赖于 Number 属性。因此,如果 Number 属性发生更改,代码将通知 IsOddNumber 和 IsEvenNumber 已更改:
public partial class Test : INotifyPropertyChanged
{
int _number;
public int Number
{
get { return _number; }
set
{
if (_number == value)
return;
_number = value;
OnPropertyChanged("Number");
OnPropertyChanged("IsOddNumber");
OnPropertyChanged("IsEvenNumber");
}
}
public bool IsOddNumber { get { return (Number % 2) != 0; } }
public bool IsEvenNumber { get { return !IsOddNumber; } }
}
如您所见,如果从头开始实现 INotifyPropertyChanged 接口,对于一个简单的属性设置器就需要编写很多行代码。如果您使用 WatchableObject 来实现,则可以这样编写:
public class Test : WatchableObject
{
int _number;
public int Number
{
get { return _number; }
set { SetProperty("Number", ref _number, value, "IsOddNumber", "IsEvenNumber"); }
}
public bool IsOddNumber { get { return (Number % 2) != 0; } }
public bool IsEvenNumber { get { return !IsOddNumber; } }
}
自动存储
如果您使用 WatchableObject 提供的存储,则可以省略后备字段 _number,如下所示:
public class Test : WatchableObject
{
public int Number
{
get { return GetProperty("Number"); }
set { SetProperty("Number", value, "IsOddNumber", "IsEvenNumber"); }
}
public bool IsOddNumber { get { return (Number % 2) != 0; } }
public bool IsEvenNumber { get { return !IsOddNumber; } }
}
如果您想初始化属性,可以使用 GetProperty 方法的初始化器参数:
public class Test : WatchableObject
{
const int DefaultNumber = 10;
public int Number
{
get { return GetProperty("Number", () => DefaultNumber); }
set { SetProperty("Number", value, "IsOddNumber", "IsEvenNumber"); }
}
public bool IsOddNumber { get { return (Number % 2) != 0; } }
public bool IsEvenNumber { get { return !IsOddNumber; } }
}
您应该注意,初始化器参数要等到访问属性的 getter 时才会被执行。这意味着,如果您运行以下代码:
Test test = new Test();
test.PropertyChanged += (_, e) => Trace.WriteLine(e.PropertyName + " is changed.");
test.Number = 10;
您将得到以下结果:
Number is changed. IsOddNumber is changed. IsEvenNumber is changed.
自动属性名称
.NET Framework 4.5 引入了一个名为 CallerMemberName 的属性,它允许您获取调用方法的属性名称。通过此属性,可以省略属性名称,如下所示:
public class Test : WatchableObject
{
const int DefaultNumber = 10;
public int Number
{
get { return GetCallerProperty(() => DefaultNumber); }
set { SetCallerProperty(value, new string[] { "IsOddNumber", "IsEvenNumber" }); }
}
public bool IsOddNumber { get { return (Number % 2) != 0; } }
public bool IsEvenNumber { get { return !IsOddNumber; } }
}
可重构的属性名
属性名称字符串无法被 Visual Studio 重构。如果您想避免这种情况,可以使用 lambda 表达式而不是属性名称字符串,如下所示:
public class Test : WatchableObject
{
const int DefaultNumber = 10;
public int Number
{
get { return GetProperty(() => Number, () => DefaultNumber); }
set { SetProperty(() => Number, value, ToPropertyName(() => IsOddNumber, () => IsEvenNumber)); }
}
public bool IsOddNumber { get { return (Number % 2) != 0; } }
public bool IsEvenNumber { get { return !IsOddNumber; } }
}
您应该知道这会消耗一些性能。如果 C# vNext 的 nameof 运算符已实现,则应避免使用 lambda 表达式。
属性更改时运行自定义代码
SetProperty 方法在属性更改时返回 true。这允许您在属性更改时执行自定义代码:
public class Test : WatchableObject
{
const int DefaultNumber = 10;
public int Number
{
get { return GetProperty("Number", () => DefaultNumber); }
set
{
if (SetProperty("Number", value, "IsOddNumber", "IsEvenNumber"))
Trace.WriteLine("The Number property is changed.");
}
}
public bool IsOddNumber { get { return (Number % 2) != 0; } }
public bool IsEvenNumber { get { return !IsOddNumber; } }
}
性能
这是一个简单的性能测试结果,显示了 10000 次读写操作的平均滴答数。有四种情况:
- 本地存储:使用后备字段存储属性值的属性。
- 带 Lambda 的本地存储:使用后备字段存储属性值,并使用 lambda 表达式作为属性名称的属性。
- 自动存储:使用自动存储来存储属性值的属性。
- 带 Lambda 的自动存储:使用自动存储存储属性值,并使用 lambda 表达式作为属性名称的属性。
结果如下
读/写 | 本地存储 | 带 Lambda 的本地存储 | 自动存储 | 带 Lambda 的自动存储 |
---|---|---|---|---|
6 个属性 | 1 滴答 | 19 滴答 | 2 滴答 | 39 滴答 |
12 个属性 | 1 滴答 | 38 滴答 | 6 滴答 | 81 滴答 |
25 个属性 | 2 滴答 | 80 滴答 | 15 滴答 | 181 滴答 |
50 个属性 | 5 滴答 | 173 滴答 | 34 滴答 | 412 滴答 |
100 个属性 | 11 滴答 | 389 滴答 | 76 滴答 | 1003 滴答 |
如果在 Win RT 中运行,“带 Lambda 的本地存储”和“带 Lambda 的自动存储”的性能会差很多。您可以在 nicenis.codeplex.com 下载性能测试程序。
支持的预处理器符号
您可以使用以下预处理器符号:
- NICENIS_RT:如果您想为 Win RT 编译,请定义此符号。
- NICENIS_4C:如果您想为 .NET Framework 4 客户端配置文件编译,请定义此符号。