NotifyPropertyChanged event where event args contain the old value
Asked Answered
K

5

22

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.

Kristianson answered 6/10, 2011 at 17:0 Comment(0)
K
42

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!

Kristianson answered 12/10, 2011 at 15:52 Comment(5)
My microbench marks proved to generate similar results, however depending on how much garbage each notification is generating, I found that excessively calling INPC with expressions put extra pressure on the GC and ended up causing a lot more Gen1 collections. All though the design was not great (and plenty of other things could be improved), changing back to strings gave us a visible performance improvement in one specific WPF application.Dowie
I tried to use this code and have the ViewModel to implement the interface INotifyPropertyChangedExtended<T> instead of the usual INotifyPropertyChanged, but I didn't get a two way binding.Ginseng
Two way binding works for me. Probably there's something you missed ... But keep in mind you DON'T need to implement the INotifyPropertyChangedExtended<T> interface. In your class you can still just implement INotifyPropertyChanged, so your class definition doesn't change ... I found that part with INotifyPropertyChangedExtended<T> a bit confusing though. Point is, DON'T implement that interface, just use PropertyChangedExtendedEventArgs.Slinky
"I found that part with INotifyPropertyChangedExtended<T> a bit confusing though" -- I concur with @lightxx's comment. It would only make sense to have a generic interface if you can guarantee that all properties have the same type T, which hardly seems that useful. In addition, with your generic NotifyPropertyChanged<T>() method, it's not even clear how the code you posted is supposed to compile, as you're not even implementing the generic interface, never mind raising the event in that interface. This answer might have some useful ideas, but it's half-baked and is confusing novices.Lucretialucretius
I am not sure, why are you using a generic declaration and not Object as type? It makes the things more easy to use and the look and feel is like the standard INotifyPropertyChanged.Laicize
M
4

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'.
Moshemoshell answered 14/6, 2019 at 19:12 Comment(0)
M
3

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

Mcwhorter answered 6/10, 2011 at 17:6 Comment(3)
Neither of the event args for those two events allow the handler to determine what the value is changing from or two. Furthermore, the handling class can't be expected to 1) store a list of all the properties that fire a changing event, 2) Use reflection to get the value of all of those properties when the events are fired, so that they can 3) Compare a stored value of the property that was changed before and after the changing and changed event were fired.Kristianson
1) Why would an object who is listening for INotifyPropertyChanging not be in charge of why it is listening for the event? From an abstract point of view I think it would be unwise for any programmer to assume why someone is listening for an event (beyond knowing the event happened and why). If you plan on reusing your INotifyPropertiesChangedAndChangingWithValues then extend. If it's being used once, a new interface seems like additional work for little if any advantages.Mcwhorter
Because 1) it takes the handler order N time to do it and the event notifier order 1 time. 2) The event notifier is the information expert. 3) Having the handler track all the properties in the object who's property changed event it is monitoring is scope creep and bad design.Kristianson
P
1

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.

Pistachio answered 6/10, 2011 at 17:5 Comment(1)
Property Change Notifications require that the property be assigned its new value before it's called. See the .Net Framework documentation: msdn.microsoft.com/en-us/library/ms743695.aspx. All I want is to include additional information.Kristianson
M
1

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.

Merous answered 6/10, 2011 at 17:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.