简体   繁体   中英

NotifyPropertyChanged event where event args contain the old value

Is there an INotifyPropertyChanged-like interface where the event args contains the old value of the property being changed, or do I have to extend that interface to create one?

For example:

    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) );
    }

I would also settle for a PropertyChanging-like event which provides this information, whether or not it supports e.Cancel.

As indicated by the answers, I had to implement my own solution. For the benefit of others, I've presented it here:

The Extended PropertyChanged Event

This event has been specially designed to be backwards compatible with old propertyChanged events. It can be used interchangeably with the simple PropertyChangedEventArgs by callers. Of course, in such cases, it is the responsibility of the event handler to check if the passed PropertyChangedEventArgs can be downcast to a PropertyChangedExtendedEventArgs, if they want to use it. No downcasting is necessary if all they're interested in is the PropertyName property.

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;
    }
}

Example 1

The user can now specify a more advanced NotifyPropertyChanged method that allows property setters to pass in their old value:

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

Where your new NotifyPropertyChanged method looks like this:

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

And OnPropertyChanged is the same as always:

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

Example 2

Or if you prefer to use lambda expressions and do away with hard-coded property name strings entirely, you can use the following:

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

Which is supported by the following magic:

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;
}

Performance

If performance is a concern, see this question: Implementing NotifyPropertyChanged without magic strings .

In summary, the overhead is minimal. Adding the old value and switching to the extended event is about a 15% slowdown, still allowing for on the order of one million property notifications per second, and switching to lambda expressions is a 5 times slowdown allowing for approximately one hundred thousand property notifications per second. These figures are far from being able to form a bottleneck in any UI-driven application.


(Optional) The Extended PropertyChanged Interface

Note: You do not have to do this. You can still just implement the standard INotifyPropertyChanged interface.

If the programmer wants to create an event that requires notifying properties to include an old value and a new value, they would need to define and implement the following interface:

// 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);

Now anyone hooking the PropertyChanged event needs to supply the extended args defined above. Note that depending on your use case, your UI may still require you to implement the basic INotifyPropertyChanged interface and event, which would conflict with this one. This is the sort of thing you would do if, for instance, you were building your own UI elements that hinged on this behaviour.


8 Years Later FAQ - How do I use it?

The above examples show how you would send the new property information, but not how you would consume them. 8 Years late, but here's an example of an implementation of the event (shout-out to @Paddy for filling in for the deficiency the past 6 years):

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
}

As we note in the above example, there's not much we can do with these generic arguments without casting first. That's because 8 years ago, I may or may not have even known what covariance was. If you would like this to be even more useful, it may make sense to define some interfaces you can use to do type checking and extract property values without knowing the runtime type:

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;
    }
}

This is now much nicer to use:

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

I hope that clears things up!

The accepted answer is great, but I struggled to follow how the PropertyChangedExtendedEventArgs<T> was supposed to be implemented, I eventually realized it wasn't.

Below is a full working example showing how to use the 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; }
    }
}

Output:

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

Sounds like you want to use the INotifyPropertyChanging in conjunction with INotifyPropertyChanged . Msdn Documentation http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanging.aspx

If you only want to old value you can invoke the event before changing the property's value. But that would be a departure of how this event is normally used, so I would create a dedicated interface and args for it.

Nope, you have to create your own from scratch.

I used to do the same in my research project, Granite, but I came to the conlcusion that it wasn't worth the cost. Too many properties I work with are calculated, and having to run them twice just to raise an event was too costly.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM