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

Posted

技术标签:

【中文标题】如何在不使用字符串名称的情况下引发 PropertyChanged 事件【英文标题】:How to raise PropertyChanged event without using string name 【发布时间】:2011-03-12 14:47:21 【问题描述】:

最好能够在不明确指定更改的属性名称的情况下引发“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));
        
    

在这种情况下,接收到的名称是 lambda 方法的名称:“b__0”。

    我可以确定,修剪“b__0”将始终提供正确的属性名称吗? 是否有其他人可以通知财产变更(来自财产本人)?

谢谢。

【问题讨论】:

【参考方案1】:

添加了 C# 6 答案

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

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

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

nameof 的参考信息在这里:https://msdn.microsoft.com/en-us/library/dn986596.aspx

例子:

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;  

原始 C# 5 答案

自 C# 5 起,最好使用 caller info 属性,这是在编译时解决的,不需要反射。

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

或者,您可以将此方法用于自我更改通知,并将@Jehof 给出的答案用于相关属性更改通知......这将具有没有魔术字符串的优点,对于 self 的常见情况执行速度最快更改通知。

这个最新的建议在下面实现(我想我会开始使用它!)

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;  

【讨论】:

谢谢。这个INPC 让生活变得非常轻松,无需担心属性名称的更改和拼写错误。【参考方案2】:

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

这是我的泛型版本,它遵循 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;

现在你可以在这样的类上使用它:

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

最后,您可以监听该对象上的事件,并使用 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
    

【讨论】:

【参考方案3】:

我正在使用扩展方法

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

        return memberExpression.Member.Name;
    

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

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

    OnPropertyChanged(e);

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

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

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

【讨论】:

完美解决方案!知道使用反射会如何影响性能吗? 没关系,我发现这篇文章提到了不同的实现如何执行:pochet.net/blog/2010/06/25/…【参考方案4】:

我使用一个简单的扩展方法来获取属性名称以避免魔术字符串出现问题。它还保持代码的可读性,即明确发生了什么。

扩展方法简单如下:

public static string GetPropertyName(this MethodBase methodBase)

    return methodBase.Name.Substring(4);

这意味着您的属性集可以抵御名称更改,如下所示:

private string _name;
public string Name

    get  return _name; 
    set 
    
            name = value;
            RaisePropertyChanged(MethodBase.GetCurrentMethod().GetPropertyName()); 
    

我已经写了更多关于这个extension method here 和published a matching code snippet here。

【讨论】:

【参考方案5】:

这是我发现的方法:

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

更新:原始代码对 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));
        
    

用法如下。


您可以在调用属性 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);
        
    

以下是如何在类中使用该帮助器来引发一个或多个属性的事件:

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

请注意,如果 PropertyChangednull,此帮助程序也是无操作的。

【讨论】:

谢谢你的回答,它给了我一些帮助......但是这里的“目标”对象代表什么?使用您的代码,我可以了解如何访问属性名称,但总体思路对我来说并不清楚。如果您能解释一下,我将不胜感激。谢谢! 您创建一个实现 INotifyPropertyChanged 和特定属性的对象。在我的示例中,我有一个具有 Now、Age 和 Weight 属性的对象。然后,当您想要为这些属性中的一个或多个引发通知时,您可以使用调用该属性的 getter 的 lambda 表达式对对象实例的 PropertyChanged 事件调用 Raise 扩展方法。扩展方法使用 lambda 上的反射来查找属性的名称,然后调用事件的实际处理程序。 请注意,如果您尝试使用 PropertyChanged.Raise(() => this.Now); 此解决方案将不起作用在继承的类上。您将需要在基类上使用类似于 protected void OnPropertyChanged(params Expression>[] propertyExpressions) PropertyChanged.Raise(propertyExpressions); 的方法。 这在某些情况下不再适用于 C# 6。出于性能原因,Roslyn 团队更改了编译器声明 lambda 的方式,您会发现在某些(闭包相关)场景下,表达式可能不是 ConstantExpression。 嗯,我写那段代码已经快五年了。事情一定会在某个时候发生变化。 :-) @TomDeloford 是否愿意提供更新的代码?【参考方案7】:

有几种方法可以在不使用属性名的情况下执行此操作。

最好只阅读博客。

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

http://justinangel.net/AutomagicallyImplementingINotifyPropertyChanged

【讨论】:

【参考方案8】:

在下面的示例中,您必须传递 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()));
        
    


以下代码包含从 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));
        
    


最后是一些测试代码:

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

【讨论】:

以上是关于如何在不使用字符串名称的情况下引发 PropertyChanged 事件的主要内容,如果未能解决你的问题,请参考以下文章

如何在不引发错误的情况下检查模块的存在?

如何在不使用 strcmp 和方括号的情况下比较字符串?

如何在不引发事件的情况下设置 checkbox.isChecked

如何在不引发错误的情况下更新数据库中的不喜欢投票?

如何在不使用 argv 的情况下获取字符串数组 - CS50 pset2

在不退出程序的情况下引发异常