.net 如何在不使用字符串名称的情况下引发 PropertyChanged 事件

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3191536/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-03 14:30:02  来源:igfitidea点击:

How to raise PropertyChanged event without using string name

.netreflectionbindinglambdainotifypropertychanged

提问by Budda

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:

如果能够在不显式指定已更改属性的名称的情况下引发 'PropertyChanged' 事件,那将会很好。我想做这样的事情:

    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".

在这种情况下,收到的名称是 lambda 方法的名称:“<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)?
  1. 我可以确定,修剪 "<set_" 和 ">b__0" 将始终提供正确的属性名称吗?
  2. 是否还有其他人需要通知有关财产变更的信息(来自财产本人)?

Thank you.

谢谢你。

回答by TCC

Added C# 6 Answer

添加了 C# 6 答案

In C# 6 (and whatever version of VB comes with Visual Studio 2015) we have the nameofoperator 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 nameofoperator can be used in all cases, and is especially useful in the "related-property-changed" notification scenario.

在 C# 6(以及 Visual Studio 2015 附带的任何版本的 VB)中,我们有一个nameof运算符,使事情变得比以往任何时候都容易。在下面的原始答案中,我使用 C# 5 功能(调用者信息属性)来处理“自我更改”通知的常见情况。该nameof操作员可在所有情况下使用,并且是特别有用的“关联属性更改”通知情景。

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 nameofoperator 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).

为简单起见,我想我会保留调用者信息属性方法来处理常见的自我更改通知。更少的输入意味着更少的拼写错误和复制/粘贴引起的错误......这里的编译器确保您选择有效的类型/成员/变量,但并不能确保您选择正确的类型。然后使用 newnameof运算符进行相关属性更改通知很简单。下面的例子演示了调用者信息属性的一个关键行为......如果参数是由调用者指定的,则该属性对参数没有影响(即,只有当参数被省略时,才为参数值提供调用者信息)呼叫者,召集者)。

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

还值得注意的是,该nameof运算符也可以被 PropertyChanged 事件处理程序使用。现在,您可以使用运算符PropertyName将事件中的值(即 a string)与特定属性进行比较nameof,从而消除更多魔术字符串。

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

nameof此处的参考信息: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

原始 C# 5 答案

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

从 C# 5 开始,最好使用调用者信息属性,这是在编译时解决的,不需要反射。

I implement this in a base class, derived classes just call the OnPropertyChangedmethod 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.

我在基类中实现了这个,派生类只是OnPropertyChanged从它们的属性设置器中调用方法。如果某些属性隐式地更改了另一个值,我也可以在属性设置器中使用该方法的“显式”版本,这样就不再“安全”,但这是一种我接受的罕见情况。

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.

或者,您可以将此方法用于自我更改通知,并使用@Jehof 给出的答案进行相关属性更改通知......

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

回答by Franci Penov

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):

更新:原始代码不是 Windows Phone 友好的,因为它依赖于 LambdaExpression.Compile() 来获取事件源对象。这是更新的扩展方法(也删除了参数检查):

    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:

您可以在调用属性 getter 的 lambda 函数上使用反射来获取属性名称。请注意,您实际上不必调用该 lambda,您只需要它进行反射:

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 PropertyChangedis null.

请注意,在PropertyChangedis 的情况下,此帮助程序也是无操作的null

回答by Tim Murphy

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.

在下面的示例中,您必须传递 3 个值(支持字段、新值、作为 lambda 的属性),但没有魔术字符串,并且只有在真正不相等时才会引发属性更改事件。

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.

以下代码包含用于从 lambda 表达式获取属性名称的扩展方法。

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

回答by Jehof

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).

结合以下方法。该方法在实现 INotifyPropertyChanged 接口的类中定义(通常是派生我的其他类的基类)。

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

然后我可以按如下方式引发 PropertyChanged-Event

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.

使用这种方法,很容易重命名属性(在 Visual Studio 中),因为它确保相应的 PropertyChanged 调用也被更新。

回答by TugboatCaptain

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 PropertyChangedEventArgsclass. So any code that actually uses your PropertyChangedevent 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.

已经发布的解决方案有两个问题的混合:
1)有些要求您创建一个基类并从中继承。这是一个巨大的问题,它可能会破坏您的类继承链,并导致您开始重新设计您的域,只是为了允许像这样的“额外”开发。
2) 虽然现有的解决方案允许您通过 lambda 表达式指定要在哪个属性上触发更改的事件,但它们仍然记录和分发属性名称的字符串表示,因为它们依赖于现有的PropertyChangedEventArgs类。所以任何实际使用您的PropertyChanged 的代码事件仍然必须进行字符串比较,这再次破坏了您将来可能需要进行的任何自动重构,更不用说您的编译时支持不在窗口之内,这是第一个允许 lambda 表达式而不是字符串的要点之一地方。

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.

这是我的泛型版本,它遵循由 MS 启动的相同事件/委托模式,这意味着不需要基类和扩展方法。

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!

最后,您可以侦听该对象上的事件,并使用 lambda 表达式确定更改了哪个属性!

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

回答by Jeff

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

回答by Simon

回答by Steven Wilber

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 hereand I've published a matching code snippet here.

我在这里写了更多关于这个扩展方法的文章,我在这里发布了一个匹配的代码片段