通知属性更改事件,其中事件参数包含旧值。

22

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

例如:

    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。

5个回答

42

根据回答,我必须实现自己的解决方案。为了让其他人受益,我在这里提供了它:

扩展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 年后 FAQ - 我该如何使用它?

上面的示例显示了如何发送新的属性信息,但没有显示如何使用它们。虽然已经过去了 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}'.");

我希望你能理解清楚!

我的微基准测试结果表明产生了类似的结果,但是根据每个通知生成多少垃圾,我发现过度使用带有表达式的INPC会给GC带来额外的压力,并最终导致更多的Gen1集合。尽管设计不太好(还有很多其他方面可以改进),但在一个特定的WPF应用程序中改回字符串确实给我们带来了可见的性能提升。 - Lee Campbell
我尝试使用这段代码,并让ViewModel实现接口INotifyPropertyChangedExtended<T>而不是通常的INotifyPropertyChanged,但我没有得到双向绑定。 - xavigonza
5
双向绑定对我有效。可能有些东西你错过了......但请记住,你不需要实现INotifyPropertyChangedExtended<T>接口。在你的类中,你仍然只需要实现INotifyPropertyChanged,所以你的类定义不会改变......我发现带有INotifyPropertyChangedExtended<T>的部分有点令人困惑。关键是,不要实现该接口,只需使用PropertyChangedExtendedEventArgs即可。 - lightxx
2
我发现 INotifyPropertyChangedExtended<T> 这部分有点令人困惑。我同意 @lightxx 的评论。只有当你能保证所有属性都具有相同的类型 T 时,才有意义使用泛型接口,这似乎并不是很有用。此外,对于你的泛型 NotifyPropertyChanged<T>() 方法,甚至不清楚你发布的代码应该如何编译,因为你甚至没有实现泛型接口,更别提在该接口中引发事件了。这个答案可能有一些有用的想法,但它是半成品的,会让初学者感到困惑。 - Peter Duniho
我不确定为什么你要使用通用声明而不是Object作为类型?这会使事情更容易使用,外观和感觉就像标准的INotifyPropertyChanged。 - gegy

4

被接受的答案很棒,但我很难理解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'.

3

这两个事件的任何一个事件参数都无法让处理程序确定值是从哪个值变化而来,也无法确定它变化为什么值。此外,不能指望处理类 1)存储所有触发更改事件的属性列表,2)在事件被触发时使用反射获取所有这些属性的值,以便可以3)比较更改和变化事件被触发之前和之后更改的属性的存储值。 - Alain
一个监听 INotifyPropertyChanging 的对象为什么不负责监听事件的原因呢?从抽象的角度来看,我认为任何程序员都不应该假设为什么有人会监听一个事件(除了知道事件发生和为什么)。 如果你计划重复使用你的 INotifyPropertiesChangedAndChangingWithValues ,那就扩展它。如果只使用一次,那么新的接口似乎是额外工作,而且优势微乎其微。 - Erik Philips
2
因为1)处理程序需要N次时间来执行,而事件通知程序只需要1次时间。2)事件通知程序是信息专家。3)让处理程序跟踪它正在监视的属性更改事件对象中的所有属性是范围膨胀和糟糕的设计。 - Alain

1

如果你只想要旧值,你可以在改变属性值之前调用事件。但这将偏离此事件通常的使用方式,因此我会为此创建一个专门的接口和参数。


属性更改通知要求在调用之前为属性分配其新值。请参阅 .Net Framework 文档:http://msdn.microsoft.com/en-us/library/ms743695.aspx。我只想包含额外的信息。 - Alain

1

不行,你必须从头开始创建自己的。

我曾经在我的研究项目Granite中做过同样的事情,但我得出结论,这并不值得成本。我处理的属性太多是计算出来的,而为了触发事件而运行两次它们的代价太高了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接