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

PropertyObservable - INotifyPropertyChanged 接口的基类。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (4投票s)

2015年7月19日

Ms-PL

4分钟阅读

viewsIcon

19117

downloadIcon

229

PropertyObservable 的介绍,它是一个实现 INotifyPropertyChanged 接口的基类。

引言

如果您是 .NET 开发人员,特别是 MVVM 的 WPF 开发人员,您一定知道 INotifyPropertyChanged 接口的重要性。这是一个几乎每天都会用到的核心接口。因此,如果我们优化 INotifyPropertyChanged 的基类实现,即使改进很小,也可能大大提高生产力。PropertyObservable 类是我对 INotifyPropertyChanged 的第二次实现。(您可以在这里看到第一次实现)它的设计目标是最大限度地提高生产力。

基本的 INotifyPropertyChanged 实现

INotifyPropertyChanged 是一个非常简单的接口。它只声明了 PropertyChanged 事件

// Notifies clients that a property value has changed.
public interface INotifyPropertyChanged
{
    // Occurs when a property value changes.
    event PropertyChangedEventHandler PropertyChanged;
}

PropertyChanged 事件可以用几行代码轻松实现,如下所示:

public class PropertyObservable : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler propertyChanged = PropertyChanged;
        if (propertyChanged != null)
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

但这并非故事的全部。您必须正确编写属性,以便在它们被更改时触发 PropertyChanged 事件。

编写属性

如果您使用基本的 INotifyPropertyChanged 实现来编写属性,它看起来会是这样的:

public partial class Test : PropertyObservable
{
    int _number;

    public int Number
    {
        get { return _number; }
        set
        {
            if (_number == value)
                return;

            _number = value;
            OnPropertyChanged("Number");
        }
    }
}

我将称之为“原始属性”。原始属性本身并不差,但原始属性的 setter 有点冗长。PropertyObservable 类为此情况提供了名为 SetProperty 的方法。可以使用 SetProperty 方法重写原始属性的 setter,如下所示:

public partial class Test : PropertyObservable
{
    int _number;

    public int Number
    {
        get { return _number; }
        set { SetProperty(ref _number, value, "Number"); }
    }
}

如果您使用 .NET Framework 4.5 或更高版本,可以省略属性名称,如下所示:

public partial class Test : PropertyObservable
{
    int _number;

    public int Number
    {
        get { return _number; }
        set { SetProperty(ref _number, value); }
    }
}

为了简洁起见,我将假设您使用 .NET Framework 4.5 或更高版本。在大多数情况下,上述属性实现已经足够。它速度快,并且推荐所有用户使用。但它需要管理一个私有字段。此外,在我看来,当定义多个带注释的属性时,它看起来有点乱。

public partial class Test : PropertyObservable
{
    int _number1;

    /// <summary>
    /// The number 1.
    /// </summary>
    public int Number1
    {
        get { return _number1; }
        set { SetProperty(ref _number1, value); }
    }

    int _number2;

    /// <summary>
    /// The number 2.
    /// </summary>
    public int Number2
    {
        get { return _number2; }
        set { SetProperty(ref _number2, value); }
    }
}

无论如何,还有另一个 SetProperty 方法不需要私有字段。Number 属性可以不使用私有字段重写,如下所示:

public partial class Test : PropertyObservable
{
    public int Number
    {
        get { return GetProperty<int>(); }
        set { SetProperty(value); }
    }
}

GetProperty 方法检索存储在内部存储中的属性值。上述属性实现比之前的属性实现慢,但在我看来,它看起来很整洁,并且生产力略高。以下示例显示了两个带注释的属性:

public partial class Test : PropertyObservable
{
    /// <summary>
    /// The number 1.
    /// </summary>
    public int Number1
    {
        get { return GetProperty<int>(); }
        set { SetProperty(value); }
    }

    /// <summary>
    /// The number 2.
    /// </summary>
    public int Number2
    {
        get { return GetProperty<int>(); }
        set { SetProperty(value); }
    }
}

如果您需要设置默认属性值,可以使用 GetProperty 方法的 getDefault 参数:

public partial class Test : PropertyObservable
{
    public int Number
    {
        get { return GetProperty<int>(getDefault: () => 7); }
        set { SetProperty(value); }
    }
}

检查属性更改

当属性发生更改时运行自定义代码是很常见的。如果您需要知道属性何时发生更改,最著名的方法是处理 PropertyChanged 事件。

public partial class Test : PropertyObservable
{
    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);

        if (e.PropertyName == "Number")
        {
            Debug.WriteLine("Number: " + Number);
            return;
        }
    }
}

但是 PropertyChanged 事件有一个缺点。它不提供旧的属性值。因此 PropertyObservable 提供了另一个名为 PropertyValueChanged 的事件,该事件提供了旧的属性值。

public partial class Test : PropertyObservable
{
    protected override void OnPropertyValueChanged(IPropertyValueChangedEventArgs e)
    {
        base.OnPropertyValueChanged(e);

        if (e.PropertyName == "Number")
        {
            Debug.WriteLine("Number: {0} -> {1}", e.OldValue, e.NewValue);
            return;
        }
    }
}

但请注意,仅当您使用 SetProperty 方法设置属性值时,才会引发该事件。此外,OldValueNewValue 不是类型安全的。

有时为了代码的可管理性,在属性 setter 中处理属性更改是合理的。在原始 Number 属性 setter 中,上述代码可以重写为:

public partial class Test : PropertyObservable
{
    int _number;

    public int Number
    {
        get { return _number; }
        set
        {
            if (_number == value)
                return;

            int oldValue = _number;
            _number = value;
            OnPropertyChanged("Number");
            Debug.WriteLine("Number: {0} -> {1}", oldValue, value);
        }
    }
}

如果您在属性 setter 中使用 SetProperty 方法,则可以简化。SetProperty 方法在属性更改时返回 true。因此,上述代码可以重写为:

public partial class Test : PropertyObservable
{
    public int Number
    {
        get { return GetProperty<int>(); }
        set
        {
            int oldValue = Number;
            if (SetProperty(value))
                Debug.WriteLine("Number: {0} -> {1}", oldValue, value);
        }
    }
}

或者您可以使用 SetProperty 方法的 onChanged 参数。它是一个回调委托,在属性更改时被调用。它还在其委托参数中提供了旧的属性值。因此,您无需缓存旧的属性值。

public partial class Test : PropertyObservable
{
    public int Number
    {
        get { return GetProperty<int>(); }
        set { SetProperty(value, onChanged: p => Debug.WriteLine("{0}: {1} -> {2}", p.PropertyName, p.OldValue, p.NewValue)); }
    }
}

调整属性值

在设置属性值之前,经常需要对其进行调整,以修复无效输入或其他原因。调整属性值很容易。例如,以下代码显示了一个不允许负数的原始属性 setter:

public partial class Test : PropertyObservable
{
    int _number;

    public int Number
    {
        get { return _number; }
        set
        {
            if (_number == value)
                return;
                
            if (value < 0)
            {
                Debug.WriteLine("Number: {0} >> 0", value);
                value = 0;
                
                if (_number == value)
                    return;
            }

            _number = value;
            OnPropertyChanged("Number");
        }
    }
}

如果您在属性 setter 中使用 SetProperty 方法,它看起来会是这样:

public partial class Test : PropertyObservable
{
    public int Number
    {
        get { return GetProperty<int>(); }
        set
        {
            if (Number == value)
                return;
        
            if (value < 0)
            {
                Debug.WriteLine("Number: {0} >> 0", value);
                value = 0;
            }

            SetProperty(value);
        }
    }
}

SetProperty 方法还提供了 onChanging 参数,该参数在属性更改之前被调用。它允许您通过其委托参数覆盖新的属性值。因此,您可以使用 onChanging 参数,如下所示:

public partial class Test : PropertyObservable
{
    public int Number
    {
        get { return GetProperty<int>(); }
        set
        {
            SetProperty(value, onChanging: p =>
            {
                if (p.NewValue < 0)
                {
                    Debug.WriteLine("{0}: {1} >> 0", p.PropertyName, p.NewValue);
                    p.NewValue = 0;
                }
            });
        }
    }
}

如果您想在集中位置调整属性值,可以使用 PropertyValueChanging 事件,如下所示:

public partial class Test : PropertyObservable
{
    protected override void OnPropertyValueChanging(IPropertyValueChangingEventArgs e)
    {
        base.OnPropertyValueChanging(e);

        if (e.PropertyName == "Number")
        {
            if (((int)e.NewValue != Number && (int)e.NewValue < 0)
            {
                Debug.WriteLine("Number: {0} >> 0", e.NewValue);
                e.NewValue = 0;
            }

            return;
        }
    }
}

但请注意,与 PropertyValueChanged 事件一样,仅当您使用 SetProperty 方法设置属性值时,才会引发 PropertyValueChanging 事件。此外,OldValueNewValue 不是类型安全的。并且 PropertyValueChanging 事件在 onChaning 回调被调用之前被引发。

支持的预处理器符号

您可以使用以下预处理器符号:

  • NICENIS_4C:如果要在 .NET Framework 4 Client Profile 中进行编译,请定义此符号。
© . All rights reserved.