在没有魔术字符串的情况下实现NotifyPropertyChanged

时间:2011-10-11 15:23:28

标签: wpf binding .net-3.5 inotifypropertychanged

  

可能重复:
  typesafe NotifyPropertyChanged using linq expressions

我正在研究一个庞大的团队应用程序,它正在以NotifyPropertyChanged("PropertyName")的形式大量使用魔术字符串,这是咨询Microsoft时的标准实现。我们还遭受了大量错误命名的属性(使用具有数百个存储的计算属性的计算模块的对象模型) - 所有这些属性都绑定到UI。

我的团队遇到许多与属性名称更改相关的错误,导致错误的魔术字符串和破坏绑定。我希望通过实现属性更改通知而不使用魔术字符串来解决问题。我发现.Net 3.5的唯一解决方案涉及lambda表达式。 (例如:Implementing INotifyPropertyChanged - does a better way exist?

我的经理非常担心从

转换的性能成本
set { ... OnPropertyChanged("PropertyName"); }

set {  ... OnPropertyChanged(() => PropertyName); }

中提取名称
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
    MemberExpression body = selectorExpression.Body as MemberExpression;
    if (body == null) throw new ArgumentException("The body must be a member expression");
    OnPropertyChanged(body.Member.Name);
}

考虑像电子表格这样的应用程序,当参数发生变化时,会在UI上实时重新计算和更新大约一百个值。是否会使此更改变得如此昂贵以至于会影响UI的响应能力?我现在甚至无法证明测试此更改的合理性,因为在各种项目和类中更新属性设置器需要大约2天。

5 个答案:

答案 0 :(得分:23)

我对NotifyPropertyChanged进行了全面测试,以确定切换到lambda表达式的影响。

以下是我的测试结果:

enter image description here

正如您所看到的,使用lambda表达式大约比普通硬编码字符串属性更改实现慢5倍,但是用户不应该担心,因为即使这样,它也能够每秒输出数十万个属性更改在我不那么特别的工作电脑上。因此,不再需要硬编码字符串以及能够使用一线安装程序来处理所有业务所带来的好处远远超过了我的性能成本。

测试1 使用标准的setter实现,并检查该属性是否已实际更改:

    public UInt64 TestValue1
    {
        get { return testValue1; }
        set
        {
            if (value != testValue1)
            {
                testValue1 = value;
                InvokePropertyChanged("TestValue1");
            }
        }
    }

测试2 非常相似,添加了一项功能,允许事件跟踪旧值和新值。因为这个功能将隐含在我的新基本setter方法中,所以我想看看有多少新开销是由于该功能造成的:

    public UInt64 TestValue2
    {
        get { return testValue2; }
        set
        {
            if (value != testValue2)
            {
                UInt64 temp = testValue2;
                testValue2 = value;
                InvokePropertyChanged("TestValue2", temp, testValue2);
            }
        }
    }

测试3 是橡胶遇到的道路,我将展示这种新的漂亮语法,以便在一行中执行所有可观察的属性操作:

    public UInt64 TestValue3
    {
        get { return testValue3; }
        set { SetNotifyingProperty(() => TestValue3, ref testValue3, value); }
    }

<强>实施

在我的BindingObjectBase类中,所有ViewModel最终继承的都是驱动新功能的实现。我已经删除了错误处理,因此函数的内容很明确:

protected void SetNotifyingProperty<T>(Expression<Func<T>> expression, ref T field, T value)
{
    if (field == null || !field.Equals(value))
    {
        T oldValue = field;
        field = value;
        OnPropertyChanged(this, new PropertyChangedExtendedEventArgs<T>(GetPropertyName(expression), oldValue, value));
    }
}
protected string GetPropertyName<T>(Expression<Func<T>> expression)
{
    MemberExpression memberExpression = (MemberExpression)expression.Body;
    return memberExpression.Member.Name;
}

所有三种方法都符合OnPropertyChanged例程,这仍然是标准:

public virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
        handler(sender, e);
}

<强>加成

如果有人好奇的话,PropertyChangedExtendedEventArgs是我刚刚提出的扩展标准PropertyChangedEventArgs的东西,因此扩展的实例总是可以代替基础。当使用SetNotifyingProperty更改属性时,它会利用旧值的知识,并将此信息提供给处理程序。

public class PropertyChangedExtendedEventArgs<T> : PropertyChangedEventArgs
{
    public virtual T OldValue { get; private set; }
    public virtual T NewValue { get; private set; }

    public PropertyChangedExtendedEventArgs(string propertyName, T oldValue, T newValue)
        : base(propertyName)
    {
        OldValue = oldValue;
        NewValue = newValue;
    }
}

答案 1 :(得分:4)

出于这个原因,我个人喜欢使用Microsoft PRISM的NotificationObject,我猜他们的代码是合理优化的,因为它是由Microsoft创建的。

除了保留“Magic Strings”之外,它还允许我使用RaisePropertyChanged(() => this.Value);之类的代码,因此您不会破坏任何现有代码。

如果我使用Reflector查看他们的代码,可以使用下面的代码重新创建它们的实现

public class ViewModelBase : INotifyPropertyChanged
{
    // Fields
    private PropertyChangedEventHandler propertyChanged;

    // Events
    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            PropertyChangedEventHandler handler2;
            PropertyChangedEventHandler propertyChanged = this.propertyChanged;
            do
            {
                handler2 = propertyChanged;
                PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Combine(handler2, value);
                propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2);
            }
            while (propertyChanged != handler2);
        }
        remove
        {
            PropertyChangedEventHandler handler2;
            PropertyChangedEventHandler propertyChanged = this.propertyChanged;
            do
            {
                handler2 = propertyChanged;
                PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Remove(handler2, value);
                propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2);
            }
            while (propertyChanged != handler2);
        }
    }

    protected void RaisePropertyChanged(params string[] propertyNames)
    {
        if (propertyNames == null)
        {
            throw new ArgumentNullException("propertyNames");
        }
        foreach (string str in propertyNames)
        {
            this.RaisePropertyChanged(str);
        }
    }

    protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        string propertyName = PropertySupport.ExtractPropertyName<T>(propertyExpression);
        this.RaisePropertyChanged(propertyName);
    }

    protected virtual void RaisePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler propertyChanged = this.propertyChanged;
        if (propertyChanged != null)
        {
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

public static class PropertySupport
{
    // Methods
    public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
    {
        if (propertyExpression == null)
        {
            throw new ArgumentNullException("propertyExpression");
        }
        MemberExpression body = propertyExpression.Body as MemberExpression;
        if (body == null)
        {
            throw new ArgumentException("propertyExpression");
        }
        PropertyInfo member = body.Member as PropertyInfo;
        if (member == null)
        {
            throw new ArgumentException("propertyExpression");
        }
        if (member.GetGetMethod(true).IsStatic)
        {
            throw new ArgumentException("propertyExpression");
        }
        return body.Member.Name;
    }
}

答案 2 :(得分:2)

如果您担心lambda-expression-tree解决方案可能太慢,那么对其进行分析并找出答案。我怀疑破解打开表达式树所花费的时间会比UI在响应中花费的时间要小得多。

如果您发现它太慢,并且您需要使用文字字符串来满足您的性能标准,那么这是我见过的一种方法:

创建一个实现INotifyPropertyChanged的基类,并为其提供RaisePropertyChanged方法。该方法检查事件是否为空,创建PropertyChangedEventArgs并触发事件 - 所有常见的东西。

但是该方法还包含一些额外的诊断 - 它做了一些反射,以确保该类确实具有该名称的属性。如果该属性不存在,则会引发异常。如果该属性确实存在,那么它会记住该结果(例如,通过将属性名称添加到静态HashSet<string>),因此它不必再次进行反射检查。

然后你去了:你的自动化测试会在你重命名一个属性但是无法更新魔术字符串时开始失败。 (我假设您已经对ViewModel进行了自动化测试,因为这是使用MVVM的主要原因。)

如果你不想在制作中吵闹,你可以将额外的诊断代码放在#if DEBUG内。

答案 3 :(得分:1)

实际上我们讨论了这个以及我们的项目,并谈了很多利弊。最后,我们决定保留常规方法,但是使用了一个字段。

public class MyModel
{
    public const string ValueProperty = "Value";

    public int Value
    {
        get{return mValue;}
        set{mValue = value; RaisePropertyChanged(ValueProperty);
    }
}

这有助于重构,保持我们的性能,在我们使用PropertyChangedEventManager时尤为有用,我们需要再次使用硬编码字符串。

public bool ReceiveWeakEvent(Type managerType, object sender, System.EventArgs e)
{
    if(managerType == typeof(PropertyChangedEventManager))
    {
        var args = e as PropertyChangedEventArgs;
        if(sender == model)
        {
            if (args.PropertyName == MyModel.ValueProperty)
            {

            }

            return true;
        }
    }
}

答案 4 :(得分:1)

一个简单的解决方案是在编译之前简单地预处理所有文件,检测在set {...}块中定义的OnPropertyChanged调用,确定属性名称并相应地修复name参数。

你可以使用ad-hoc工具(这是我的建议),或者使用真正的C#(或VB.NET)解析器(比如可以在这里找到的那​​些:Parser for C#)。< / p>

我认为这是合理的方式。当然,它不是很优雅也不聪明,但它对运行时没有影响,并遵循Microsoft规则。

如果你想节省一些编译时间,你可以使用两种方式使用编译指令,如下所示:

set
{
#if DEBUG // smart and fast compile way
   OnPropertyChanged(() => PropertyName);
#else // dumb but efficient way
   OnPropertyChanged("MyProp"); // this will be fixed by buid process
#endif
}