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

时间:2010-07-07 02:37:00

标签: .net reflection binding lambda inotifypropertychanged

如果没有明确指定已更改属性的名称,那么有能力引发“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方法的名称:“&lt; set_MyString&gt; b__0”。

  1. 我可以确定,修剪“&lt; set_”和“&gt; b__0”会始终提供正确的属性名称吗?
  2. 是否还有其他任何关于财产变更的通知(来自财产本身)?
  3. 谢谢。

8 个答案:

答案 0 :(得分:35)

添加了C#6答案

在C#6(以及随Visual Studio 2015提供的VB版本)中,我们拥有nameof运算符,它使事情变得前所未有的简单。在我下面的原始答案中,我使用C#5功能(调用者信息属性)来处理“自我更改”通知的常见情况。 nameof运算符可以在所有情况下使用,在“相关属性更改”通知方案中特别有用。

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

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

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

答案 1 :(得分: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,此帮助程序也是无操作。

答案 2 :(得分: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");
    }
}

答案 3 :(得分: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-Event

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

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

答案 4 :(得分: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.");
    }
 }

答案 5 :(得分: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
    }
}

答案 6 :(得分:1)

答案 7 :(得分: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()); 
    }
}

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