How to raise PropertyChanged event without using s

2020-01-26 04:23发布

问题:

It would be good to have ability to raise 'PropertyChanged' event without explicit specifying the name of changed property. I would like to do something like this:

    public string MyString
    {
        get { return _myString; }
        set
        {
            ChangePropertyAndNotify<string>(val=>_myString=val, value);
        }
    }

    private void ChangePropertyAndNotify<T>(Action<T> setter, T value)
    {
        setter(value);
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(setter.Method.Name));
        }
    }

In this case received name is a name of lambda-method: "<set_MyString>b__0".

  1. Can I be sure, that trimming "<set_" and ">b__0" will always provide the correct property name?
  2. Is there any other to notify about property changed (from property himself)?

Thank you.

回答1:

Added C# 6 Answer

In C# 6 (and whatever version of VB comes with Visual Studio 2015) we have the nameof operator which makes things easier than ever. In my original answer below, I use a C# 5 feature (caller info attributes) to handle the common case of "self-changed" notifications. The nameof operator can be used in all cases, and is especially useful in the "related-property-changed" notification scenario.

For simplicity, I think I'll keep the caller info attribute approach for common self-changed notifications. Less typing means less chances for typos and copy/paste induced bugs... the compiler here ensures that you pick a valid type/member/variable, but it doesn't ensure you pick the correct one. It is simple to then use the new nameof operator for related-property change notifications. The example below demonstrates a key behavior of caller info attributes... the attribute has no effect on a parameter if the parameter is specified by the caller (that is, the caller info is provided for the parameter value only when the parameter is omitted by the caller).

It is also worth observing that the nameof operator can be used by PropertyChanged event handlers as well. Now you can compare the PropertyName value in the event (which is a string) to a particular property using the nameof operator, eliminating more magic strings.

Reference info for nameof here: https://msdn.microsoft.com/en-us/library/dn986596.aspx

Example:

public class Program
{
    void Main()
    {
        var dm = new DataModel();
        dm.PropertyChanged += propertyChangedHandler;
    }

    void propertyChangedHandler(object sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == nameof(DataModel.NumberSquared))
        {
            //do something spectacular
        }
    }
}


public class DataModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
}

public class DataModel : DataModelBase
{
    //a simple property
    string _something;
    public string Something 
    { 
        get { return _something; } 
        set { _something = value; OnPropertyChanged(); } 
    }

    //a property with another related property
    int _number;
    public int Number
    {
        get { return _number; }

        set 
        { 
            _number = value; 
            OnPropertyChanged(); 
            OnPropertyChanged(nameof(this.NumberSquared)); 
         }
    }

    //a related property
    public int NumberSquared { get { return Number * Number; } }
}

Original C# 5 answer

Since C# 5, best to use caller info attributes, this is resolved at compile time, no reflection necessary.

I implement this in a base class, derived classes just call the OnPropertyChanged method from within their property setters. If some property implicitly changes another value, I can use the "Explicit" version of the method in the property setter as well, which then is no longer "safe" but is a rare situation that I just accept.

Alternatively you could use this method for self change notifications, and use the answer given by @Jehof for related property change notifications ... this would have the advantage of no magic strings, with the fastest execution for the common case of self change notifications.

This latest suggestion is implemented below (I think I'll start using it!)

public class DataModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        OnPropertyChangedExplicit(propertyName);
    }

    protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection)
    {
        var memberExpression = (MemberExpression)projection.Body;
        OnPropertyChangedExplicit(memberExpression.Member.Name);
    }

    void OnPropertyChangedExplicit(string propertyName)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
}

public class DataModel : DataModelBase
{
    //a simple property
    string _something;
    public string Something 
    { 
        get { return _something; } 
        set { _something = value; OnPropertyChanged(); } 
    }

    //a property with another related property
    int _number;
    public int Number
    {
        get { return _number; }

        set 
        { 
            _number = value; 
            OnPropertyChanged(); 
            OnPropertyChanged(() => NumberSquared); 
         }
    }

    //a related property
    public int NumberSquared { get { return Number * Number; } }
}


回答2:

Update: The original code is not Windows Phone friendly, as it relies on LambdaExpression.Compile() to get the event source object. Here's the updated extension method (with parameter checks removed as well):

    public static void Raise<T>(this PropertyChangedEventHandler handler, Expression<Func<T>> propertyExpression)
    {
        if (handler != null)
        {
            var body = propertyExpression.Body as MemberExpression;
            var expression = body.Expression as ConstantExpression;
            handler(expression.Value, new PropertyChangedEventArgs(body.Member.Name));
        }
    }

The usage stays as below.


You can get the property name using reflection on a lambda function that calls the property getter. note that you don't actually have to invoke that lambda, you just need it for the reflection:

public static class INotifyPropertyChangedHelper
{
    public static void Raise<T>(this PropertyChangedEventHandler handler, Expression<Func<T>> propertyExpression)
    {
        if (handler != null)
        {
            var body = propertyExpression.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("'propertyExpression' should be a member expression");

            var expression = body.Expression as ConstantExpression;
            if (expression == null)
                throw new ArgumentException("'propertyExpression' body should be a constant expression");

            object target = Expression.Lambda(expression).Compile().DynamicInvoke();

            var e = new PropertyChangedEventArgs(body.Member.Name);
            handler(target, e);
        }
    }

    public static void Raise<T>(this PropertyChangedEventHandler handler, params Expression<Func<T>>[] propertyExpressions)
    {
        foreach (var propertyExpression in propertyExpressions)
        {
            handler.Raise<T>(propertyExpression);
        }
    }
}

Here's how you can use that helper in your class to raise the event for one or multiple properties:

PropertyChanged.Raise(() => this.Now);
PropertyChanged.Raise(() => this.Age, () => this.Weight);

Note that this helper also is a no-op in case the PropertyChanged is null.



回答3:

In the following example you have to pass 3 values (backing field, new value, property as lambda) but there are no magic strings and property changed event is only raised when it truly isn't equal.

class Sample : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { this.SetProperty(ref _name, value, () => this.Name); }
    }


    protected void SetProperty<T>(ref T backingField, T newValue, Expression<Func<T>> propertyExpression)
    {
        if (backingField == null && newValue == null)
        {
            return;
        }

        if (backingField == null || !backingField.Equals(newValue))
        {
            backingField = newValue;
            this.OnPropertyChanged(propertyExpression);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyExpression.GetPropertyName()));
        }
    }

}

And the following code contains extension methods to get a property name from a lambda expression.

public static class Extensions
{
    public static string GetPropertyName<TProperty>(this Expression<Func<TProperty>> propertyExpression)
    {
        return propertyExpression.Body.GetMemberExpression().GetPropertyName();
    }

    public static string GetPropertyName(this MemberExpression memberExpression)
    {
        if (memberExpression == null)
        {
            return null;
        }

        if (memberExpression.Member.MemberType != MemberTypes.Property)
        {
            return null;
        }

        var child = memberExpression.Member.Name;
        var parent = GetPropertyName(memberExpression.Expression.GetMemberExpression());

        if (parent == null)
        {
            return child;
        }
        else
        {
            return parent + "." + child;
        }
    }

    public static MemberExpression GetMemberExpression(this Expression expression)
    {
        var memberExpression = expression as MemberExpression;

        if (memberExpression != null)
        {
            return memberExpression;
        }

        var unaryExpression = expression as UnaryExpression;


        if (unaryExpression != null)
        {
            memberExpression = (MemberExpression)unaryExpression.Operand;

            if (memberExpression != null)
            {
                return memberExpression;
            }

        }
        return null;
    }

    public static void ShouldEqual<T>(this T actual, T expected, string name)
    {
        if (!Object.Equals(actual, expected))
        {
            throw new Exception(String.Format("{0}: Expected <{1}> Actual <{2}>.", name, expected, actual));
        }
    }

}

Finally some test code:

class q3191536
{
    public static void Test()
    {
        var sample = new Sample();
        var propertyChanged = 0;

        sample.PropertyChanged += 
            new PropertyChangedEventHandler((sender, e) => 
                {
                    if (e.PropertyName == "Name")
                    {
                        propertyChanged += 1;
                    }
                }
            );

        sample.Name = "Budda";

        sample.Name.ShouldEqual("Budda", "sample.Name");
        propertyChanged.ShouldEqual(1, "propertyChanged");

        sample.Name = "Tim";
        sample.Name.ShouldEqual("Tim", sample.Name);
        propertyChanged.ShouldEqual(2, "propertyChanged");

        sample.Name = "Tim";
        sample.Name.ShouldEqual("Tim", sample.Name);
        propertyChanged.ShouldEqual(2, "propertyChanged");
    }
}


回答4:

I´m using the extension method

public static class ExpressionExtensions {
    public static string PropertyName<TProperty>(this Expression<Func<TProperty>> projection) {
        var memberExpression = (MemberExpression)projection.Body;

        return memberExpression.Member.Name;
    }
}

in combination with the following method. The method is defined in the class that implements the INotifyPropertyChanged interface (Normally a base class from which my other classes are derived).

protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection) {
    var e = new PropertyChangedEventArgs(projection.PropertyName());

    OnPropertyChanged(e);
}

Then i can raise the PropertyChanged-Event as follows

private double _rate;
public double Rate {
        get {
            return _rate;
        }
        set {
            if (_rate != value) {
              _rate = value;                     
              OnPropertyChanged(() => Rate );
            }
        }
    }

Using this approach, its easy to rename Properties (in Visual Studio), cause it ensures that the corresponding PropertyChanged call is updated too.



回答5:

This is the way I found to do it:

public abstract class ViewModel<T> : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

    public void RaisePropertyChanged(Expression<Func<T, object>> expression)
    {
        var propertyName = GetPropertyFromExpression(expression);

        this.OnPropertyChanged(propertyName);
    }

    public string GetPropertyFromExpression(System.Linq.Expressions.Expression expression)
    {
        if (expression == null)
            throw new ArgumentException("Getting property name form expression is not supported for this type.");

        var lamda = expression as LambdaExpression;
        if (lamda == null)
            throw new NotSupportedException("Getting property name form expression is not supported for this type.");

        var mbe = lamda.Body as MemberExpression;
        if (mbe != null)
            return mbe.Member.Name;

        var unary = lamda.Body as UnaryExpression;
        if (unary != null)
        {
            var member = unary.Operand as MemberExpression;
            if (member != null)
                return member.Member.Name;
        }

        throw new NotSupportedException("Getting property name form expression is not supported for this type.");
    }
 }


回答6:

The solutions already posted have a mix of two issues:
1) Some require you to create a base class and inherit from it. This is a huge problem that can throw a wrench in your classes inheritance chain and cause you to start re-designing your domain just to allow a development "extra" like this.
2) While the existing solutions allow you to designate which property to fire the changed event on via a lambda expression they still record and distribute a string representation of the property's name because they rely on the existing PropertyChangedEventArgs class. So any code that actually uses your PropertyChanged event still has to do a string comparison which again breaks any automatic refactoring you may need to do in the future not to mention your compile time support is out the window which is one of the main points of allowing lambda expressions instead of strings in the first place.

This is my generics version which follows the same event/delegate pattern started by MS which means no base classes and no extension methods are necessary.

public class PropertyChangedEventArgs<TObject> : EventArgs
{
    private readonly MemberInfo _property;

    public PropertyChangedEventArgs(Expression<Func<TObject, object>> expression)
    {
        _property = GetPropertyMember(expression);
    }

    private MemberInfo GetPropertyMember(LambdaExpression p)
    {
        MemberExpression memberExpression;
        if (p.Body is UnaryExpression)
        {
            UnaryExpression ue = (UnaryExpression)p.Body;
            memberExpression = (MemberExpression)ue.Operand;
        }
        else
        {
            memberExpression = (MemberExpression)p.Body;
        }
        return (PropertyInfo)(memberExpression).Member;
    }

    public virtual bool HasChanged(Expression<Func<TObject, object>> expression)
    {
        if (GetPropertyMember(expression) == Property)
            return true;
        return false;
    }

    public virtual MemberInfo Property
    {
        get
        {
            return _property;
        }
    }
}

public delegate void PropertyChangedEventHandler<TObject>(object sender, PropertyChangedEventArgs<TObject> e);

public interface INotifyPropertyChanged<TObject>
{
    event PropertyChangedEventHandler<TObject> PropertyChanged;
}

Now you can use it on a class like this:

public class PagedProduct : INotifyPropertyChanged<PagedProduct>
{
    IPager _pager;

    public event PropertyChangedEventHandler<PagedProduct> PropertyChanged = delegate { };

    public PagedProduct() { }

    public IPager Pager
    {
        get { return _pager; }
        set
        {
            if (value != _pager)
            {
                _pager = value;
                // let everyone know this property has changed.
                PropertyChanged(this, new PropertyChangedEventArgs<PagedProduct>(a => a.Pager));
            }
        }
    }
}

And finally you can listen to the events on that object and determine which property changed using a lambda expression as well!

void SomeMethod()
{
    PagedProduct pagedProducts = new PagedProduct();
    pagedProducts.PropertyChanged += pagedProducts_PropertyChanged;
}

void pagedProducts_PropertyChanged(object sender, PropertyChangedEventArgs<PagedProduct> e)
{
    // lambda expression is used to determine if the property we are interested in has changed. no strings here
    if (e.HasChanged(a => a.Pager))
    {
        // do something mind blowing like ordering pizza with a coupon
    }
}


回答7:

There are several approaches to doing this without using a propertyname.

Best to just read the blogs.

http://www.pochet.net/blog/2010/06/25/inotifypropertychanged-implementations-an-overview/

http://justinangel.net/AutomagicallyImplementingINotifyPropertyChanged



回答8:

I use a simple extension method to get the property name to avoid problems with magic strings. It also maintains the readability of the code, i.e. it is explicit what is happening.

The extension method is simply as follows:

public static string GetPropertyName(this MethodBase methodBase)
{
    return methodBase.Name.Substring(4);
}

With this it means that you property sets are resilient against name changes and look like the following:

private string _name;
public string Name
{
    get { return _name; }
    set 
    {
            name = value;
            RaisePropertyChanged(MethodBase.GetCurrentMethod().GetPropertyName()); 
    }
}

I've written more about this extension method here and I've published a matching code snippet here.