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

使用 WatchableObject 实现 INotifyPropertyChanged 接口。

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2014年6月15日

Ms-PL

3分钟阅读

viewsIcon

10574

downloadIcon

118

介绍 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 客户端配置文件编译,请定义此符号。
© . All rights reserved.