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

41

希望能够在不显式指定更改属性名称的情况下提高“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 方法的名称:"<set_MyString>b__0"。

  1. 我可以确定,仅截取 "<set_" 和 ">b__0" 是否总是会提供正确的属性名称?
  2. 有没有其他方法可以从属性本身通知其属性已更改?

谢谢。

8个回答

44

新增 C# 6 的回答

在C# 6中(以及Visual Studio 2015捆绑的任何版本的VB),我们有了 nameof 操作符,使事情比以往更容易。在下面的原始回答中,我使用了C# 5的特性(调用方信息属性)来处理“self-changed”通知的常见情况。 nameof 操作符可用于所有情况,并且在“相关属性更改”通知方案中特别有用。

为简单起见,对于常见的自我更改通知,我认为应该保留调用方信息属性方法。打字越少,出错和粘贴复制导致错误的机会就越少... 这里的编译器确保您选择一个有效的类型/成员/变量,但它不能保证您选择正确的那个。然后可以很容易地使用新的 nameof 操作符进行相关属性更改通知。下面的示例演示了调用方信息属性的关键行为... 如果参数由调用者指定,则属性对该参数没有影响(也就是说,只有在调用者省略参数时才为参数值提供调用方信息)。

另外值得注意的是,nameof 操作符也可以由 PropertyChanged 事件处理程序使用。现在可以使用 nameof 操作符将事件中的 PropertyName 值(它是一个字符串)与特定属性进行比较,从而消除更多的魔术字符串。

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 开始,最好使用调用者信息属性,这在编译时解决,不需要反射。

我在一个基类中实现了这个功能,派生类只需在其属性设置器中调用 OnPropertyChanged 方法。如果某个属性隐式地更改了另一个值,我也可以在属性设置器中使用该方法的 "明确" 版本,但这样就不再是 "安全" 的,不过这种情况很少,我自己接受。

另外,您还可以使用这种方法进行自身更改通知,并使用 @Jehof 给出的答案来进行相关属性更改通知... 这将具有没有魔术字符串的优点,并且对于常见的自身更改通知,执行速度最快。

以下是最新的建议实现(我想我会开始使用它!)

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让生活变得非常容易,而不必担心属性名称的更改和打字错误。 - Mohsen Afshin

29

更新:原始代码不适用于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,那么该帮助程序也不会执行任何操作。


谢谢你的回答,对我有所帮助...但是这里的'target'对象代表什么?通过你的代码,我知道如何访问属性名称,但是总体思路对我来说不太清晰。如果你能解释一下,我会非常感激。谢谢! - Budda
1
你创建一个实现了 INotifyPropertyChanged 接口和特定属性的对象。在我的示例中,我有一个具有 Now、Age 和 Weight 属性的对象。然后,当您想要为其中一个或多个属性发出通知时,调用对象实例的 PropertyChanged 事件上的 Raise 扩展方法,并使用调用属性 getter 的 lambda 表达式。扩展方法使用 lambda 上的反射来查找属性的名称,然后调用事件的实际处理程序。 - Franci Penov
1
请注意,如果您尝试在继承类上使用 PropertyChanged.Raise(() => this.Now);,则此解决方案将无法工作。您需要在基类上拥有一个像这样的方法:protected void OnPropertyChanged(params Expression<Func<T>>[] propertyExpressions){ PropertyChanged.Raise(propertyExpressions);} - JoanComasFdz
1
在某些情况下,这在C# 6中已不再适用。出于性能原因,Roslyn团队更改了编译器声明lambda表达式的方式,您会发现在某些(与闭包有关的)情况下,表达式可能不是ConstantExpression常量表达式。 - Tom Deloford
嗯,我写下那段代码已经将近五年了。事情肯定会有所改变。 :-) @TomDeloford 你能否提供一份更新的代码? - Franci Penov
我正在处理这个问题!基本上,现在似乎不可能保证您可以从ConstantExpression位获取“目标”,因为这将不再必须是常量。由于超过95%的事件处理程序不查看对象源位,因此这并不是太大的问题(当然,WPF UI属性绑定不关心)。虽然情况不好,但我们可能需要开始编写Raise(this,p => p.SomeProperty)以与Roslyn编译器兼容。 - Tom Deloford

6
在下面的例子中,您需要传递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");
    }
}

5
我正在使用扩展方法。
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事件:
private double _rate;
public double Rate {
        get {
            return _rate;
        }
        set {
            if (_rate != value) {
              _rate = value;                     
              OnPropertyChanged(() => Rate );
            }
        }
    }

使用这种方法,可以轻松地重命名属性(在Visual Studio中),因为它确保相应的PropertyChanged调用也得到更新。


好的解决方案!您有想法这种反射使用会如何影响性能吗? - Fueled
1
没关系,我已经找到了这篇文章,它提到了不同的实现方式的性能:http://www.pochet.net/blog/2010/06/25/inotifypropertychanged-implementations-an-overview/ - Fueled

3
已经发布的解决方案存在两个问题的混合:
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
    }
}

2
这是我发现的做法:

这是我找到的方法:

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

1

0

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

该扩展方法如下:

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

我已经写了更多关于这个扩展方法,并且我已经发布了相应的代码片段


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接