NotifyPropertyChanged事件,其中事件参数包含旧值

本文关键字:事件 包含旧 参数 NotifyPropertyChanged | 更新日期: 2023-09-27 18:10:41

是否有一个INotifyPropertyChanged-like接口,其中事件参数包含正在更改的属性的旧值,或者我必须扩展该接口来创建一个?

例如:

    public String ProcessDescription
    {
        get { return _ProcessDescription; }
        set
        {
            if( value != ProcessDescription )
            {
                String oldValue = _ProcessDescription;
                _ProcessDescription = value;
                InvokePropertyChanged("ProcessDescription", oldvalue);
            }
        }
    }
    InvokePropertyChanged(String PropertyName, OldValue)
    {
         this.PropertyChanged( new ExtendedPropertyChangedEventArgs(PropertyName, OldValue) );
    }

我也希望有一个类似propertychanging的事件来提供这些信息,不管它是否支持e.Cancel

NotifyPropertyChanged事件,其中事件参数包含旧值

根据答案,我必须实现我自己的解决方案。为了其他人的利益,我把它呈现在这里:

扩展PropertyChanged事件

这个事件被特别设计为向后兼容旧的propertyChanged事件。它可以被调用者与简单的PropertyChangedEventArgs互换使用。当然,在这种情况下,事件处理程序有责任检查传递的PropertyChangedEventArgs是否可以向下转换为PropertyChangedExtendedEventArgs,如果它们想要使用它的话。如果他们只对PropertyName属性感兴趣,则不需要向下转换。

public class PropertyChangedExtendedEventArgs<T> : PropertyChangedEventArgs
{
    public virtual T OldValue { get; private set; }
    public virtual T NewValue { get; private set; }
    public PropertyChangedExtendedEventArgs(string propertyName, T oldValue, T newValue)
        : base(propertyName)
    {
        OldValue = oldValue;
        NewValue = newValue;
    }
}

示例1

用户现在可以指定一个更高级的NotifyPropertyChanged方法,允许属性设置器传递旧的值:

public String testString
{
    get { return testString; }
    set
    {
        String temp = testString;
        testValue2 = value;
        NotifyPropertyChanged("TestString", temp, value);
    }
}

你的新NotifyPropertyChanged方法看起来像这样:

protected void NotifyPropertyChanged<T>(string propertyName, T oldvalue, T newvalue)
{
    OnPropertyChanged(this, new PropertyChangedExtendedEventArgs<T>(propertyName, oldvalue, newvalue));
}

OnPropertyChanged一样:

public virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
        handler(sender, e);
}
示例2

或者,如果您更喜欢使用lambda表达式并完全放弃硬编码的属性名称字符串,则可以使用以下命令:

public String TestString
{
    get { return testString; }
    private set { SetNotifyingProperty(() => TestString, ref testString, value); }
}

由以下魔术支持:

protected void SetNotifyingProperty<T>(Expression<Func<T>> expression, ref T field, T value)
{
    if (field == null || !field.Equals(value))
    {
        T oldValue = field;
        field = value;
        OnPropertyChanged(this, new PropertyChangedExtendedEventArgs<T>(GetPropertyName(expression), oldValue, value));
    }
}
protected string GetPropertyName<T>(Expression<Func<T>> expression)
{
    MemberExpression memberExpression = (MemberExpression)expression.Body;
    return memberExpression.Member.Name;
}

如果性能是一个问题,请参阅这个问题:实现没有魔法字符串的NotifyPropertyChanged。

总之,开销是最小的。添加旧值并切换到扩展事件大约是15%的减速,仍然允许每秒一百万个属性通知,切换到lambda表达式是5倍的减速,允许每秒大约十万个属性通知。这些数字远远不能在任何ui驱动的应用程序中形成瓶颈。

(可选)扩展PropertyChanged接口

注意:您不必这样做。您仍然可以实现标准的INotifyPropertyChanged接口。

如果程序员想要创建一个事件,要求通知属性包含一个旧值和一个新值,他们需要定义并实现以下接口:

// Summary: Notifies clients that a property value is changing, but includes extended event infomation
/* The following NotifyPropertyChanged Interface is employed when you wish to enforce the inclusion of old and
 * new values. (Users must provide PropertyChangedExtendedEventArgs, PropertyChangedEventArgs are disallowed.) */
public interface INotifyPropertyChangedExtended<T>
{
    event PropertyChangedExtendedEventHandler<T> PropertyChanged;
}
public delegate void PropertyChangedExtendedEventHandler<T>(object sender, PropertyChangedExtendedEventArgs<T> e);

现在,任何挂钩PropertyChanged事件的人都需要提供上面定义的扩展参数。请注意,根据您的用例,您的UI可能仍然要求您实现基本的INotifyPropertyChanged接口和事件,这将与此冲突。这是你会做的事情,例如,如果你正在构建自己的UI元素,依赖于这种行为。


8年后常见问题解答-我如何使用它?

上面的示例显示了如何发送新的属性信息,但没有显示如何使用它们。晚了8年,但这里有一个事件实现的例子(向@Paddy大喊一声,因为他填补了过去6年的不足):

myNotifyingClass.PropertyChanged += OnSomePropertyChanged;
private void OnSomePropertyChanged(object sender, PropertyChangedEventArgs e)
{
    // Without casting 'e' is a standard PropertyChanged event
    Debug.WriteLine($"'{e.PropertyName}' has changed.");
    // If you just care to check whether a certain properties changed, do so as usual
    if (e.PropertyName == nameof(SomeClass.Description))
    {
        myNotifyingClass.MarkAsDirty(); // For example
    }
    // If the old/new value are if interest, you can cast in those situations
    if (e.PropertyName == nameof(SomeClass.SortKey))
    {
        // For example, use it to order by some new property first, but by the last property second.
        if(e is PropertyChangedExtendedEventArgs<string> sortKeyChanged)
            myNotifyingClass.OrderBy(sortKeyChanged.NewValue, then_by: sortKeyChanged.OldValue);
        else
            throw new Exception("I must have forgotten to use the extended args!");
    }
    // To support more general operations, see the note below on creating interfaces
}
正如我们在上面的例子中所注意到的,如果不先进行类型转换,我们对这些泛型参数做不了什么。因为8年前,我可能还不知道协方差是什么。如果您希望它更有用,那么定义一些接口可能是有意义的,您可以使用这些接口来进行类型检查和提取属性值,而无需知道运行时类型:
public interface IPropertyChangedExtendedEventArgs<out T> : IPropertyChangedEventArgs
{
    public virtual T OldValue { get; }
    public virtual T NewValue { get; }
}
public class PropertyChangedExtendedEventArgs<T> : IPropertyChangedExtendedEventArgs<T>
{
    public virtual T OldValue { get; private set; }
    public virtual T NewValue { get; private set; }
    public PropertyChangedExtendedEventArgs(string propertyName, T oldValue, T newValue)
        : base(propertyName)
    {
        OldValue = oldValue;
        NewValue = newValue;
    }
}

现在用起来好多了:

if (e is IPropertyChangedExtendedEventArgs<object> anyProperty)
    Console.WriteLine($"'{anyProperty.PropertyName}' has changed, " + 
        $"from '{anyProperty.OldValue}' to '{anyProperty.NewValue}'.");

我希望这能澄清一切!

接受的答案是伟大的,但我努力遵循PropertyChangedExtendedEventArgs<T>应该如何实现,我最终意识到它不是。

下面是一个完整的工作示例,展示了如何使用PropertyChangedExtendedEventArgs<T>

using System;
using System.ComponentModel;
namespace ConsoleApp10
{
    class Program
    {
        static void Main(string[] args)
        {
            var p = new Program();
            p.Run();
        }
        private void Run()
        {
            // Create Poco
            var poco = new MyPoco(1, "MyOldName", 150);
            // Attach property changed event
            poco.PropertyChanged += PocoOnPropertyChanged;
            // Change data
            poco.Id = 10;
            poco.Name = "NewName";
            poco.Height = 170;
        }
        /// <summary>
        /// Property changed handler
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void PocoOnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // Without casting 'e' is a standard PropertyChanged event
            if (Equals(e.PropertyName, nameof(MyPoco.Id)))
            {
                Console.WriteLine($"'{nameof(MyPoco.Id)}' has changed, but we have no other data");
            }
            // New extended property changed event of type 'string'
            if (Equals(e.PropertyName, nameof(MyPoco.Name)))
            {
                // Need to cast into type we know and are expecting
                if (e is PropertyChangedExtendedEventArgs<string> extended)
                {
                    Console.WriteLine(
                        $"'{nameof(MyPoco.Name)}' has changed, from '{extended.OldValue}' to '{extended.NewValue}'.");
                }
            }
            // New extended property changed event of type 'double'
            if (Equals(e.PropertyName, nameof(MyPoco.Height)))
            {
                // This cast will fail as the types are wrong
                if (e is PropertyChangedExtendedEventArgs<string>)
                {
                    // Should never hit here
                }
                // Cast into type we know and are expecting
                if (e is PropertyChangedExtendedEventArgs<double> extended)
                {
                    Console.WriteLine(
                        $"'{nameof(MyPoco.Height)}' has changed, from '{extended.OldValue}' to '{extended.NewValue}'.");
                }
            }
        }
    }
    /// <summary>
    /// Example POCO
    /// </summary>
    public sealed class MyPoco : NotifyBase
    {
        private int _id;
        private string _name;
        private double _height;
        public MyPoco(int id, string name, double height)
        {
            _id = id;
            _name = name;
            _height = height;
        }
        public int Id
        {
            get => _id;
            set
            {
                var old = _id;
                _id = value;
                OnPropertyChanged(old, value, nameof(Id));
            }
        }
        public string Name
        {
            get => _name;
            set
            {
                var old = _name;
                _name = value;
                OnPropertyChanged(old, value, nameof(Name));
            }
        }
        public double Height
        {
            get => _height;
            set
            {
                var old = _height;
                _height = value;
                OnPropertyChanged(old, value, nameof(Height));
            }
        }
    }
    /// <summary>
    /// Notifying base class
    /// </summary>
    public abstract class NotifyBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged<T>(T oldValue, T newValue, string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedExtendedEventArgs<T>(oldValue, newValue, propertyName));
        }
    }
    /// <summary>
    /// Extended property changed
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public sealed class PropertyChangedExtendedEventArgs<T> : PropertyChangedEventArgs
    {
        public PropertyChangedExtendedEventArgs(T oldValue, T newValue, string propertyName)
            : base(propertyName)
        {
            OldValue = oldValue;
            NewValue = newValue;
        }
        public T OldValue { get; }
        public T NewValue { get; }
    }
}
输出:

'Id' has changed, but we have no other data
'Name' has changed, from 'MyOldName' to 'NewName'.
'Height' has changed, from '150' to '170'.

听起来您想将INotifyPropertyChangingINotifyPropertyChanged结合使用。Msdn文档http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanging.aspx

如果你只想要旧的值,你可以在改变属性的值之前调用事件。但是,这将是如何使用这个事件的正常偏离,所以我将创建一个专用的接口和参数为它。

不,您必须从头开始创建自己的

我曾经在我的研究项目花岗岩中做过同样的事情,但我得出的结论是它不值得花费。我使用的太多属性都是经过计算的,为了引发一个事件而运行它们两次的代价太大了。