Реализация NotifyPropertyChanged без магических строк

Возможный дубликат:
typesafe NotifyPropertyChanged с использованием выражений linq

Я работаю над большим командным приложением, которое страдает от интенсивного использования магических строк в форме NotifyPropertyChanged("PropertyName")стандартная реализация при консультировании Microsoft. Мы также страдаем от большого числа свойств с неправильным названием (работа с объектной моделью для вычислительного модуля, который имеет сотни сохраненных вычисляемых свойств) - все они связаны с пользовательским интерфейсом.

Моя команда сталкивается со многими ошибками, связанными с изменениями имени свойства, приводящими к неправильным магическим строкам и нарушению привязок. Я хочу решить проблему путем реализации уведомлений об изменении свойств без использования магических строк. Единственные решения, которые я нашел для.Net 3.5, включают лямбда-выражения. (например: Реализация INotifyPropertyChanged - существует ли лучший способ?)

Мой менеджер крайне обеспокоен производительностью перехода с

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

Рассмотрим приложение, подобное электронной таблице, в которой при изменении параметра приблизительно сто значений пересчитываются и обновляются в пользовательском интерфейсе в режиме реального времени. Делает ли это изменение настолько дорогим, что оно повлияет на отзывчивость интерфейса? Я даже не могу оправдать тестирование этого изменения прямо сейчас, потому что для установки установщиков свойств в различных проектах и ​​классах потребуется около 2 дней.

5 ответов

Решение

Я провел тщательный тест NotifyPropertyChanged, чтобы установить влияние переключения на лямбда-выражения.

Вот результаты моего теста:

Как вы можете видеть, использование лямбда-выражения примерно в 5 раз медленнее, чем реализация простого изменения свойства строки в строгом коде, но пользователи не должны беспокоиться, потому что даже в этом случае он способен откачивать сотни тысяч изменений свойств в секунду. такой особенный рабочий компьютер. Таким образом, выгода от того, что вам больше не нужно жестко кодировать строки, и возможность иметь однострочные сеттеры, которые позаботятся обо всем вашем бизнесе, намного перевешивает затраты на производительность для меня.

Тест 1 использовал стандартную реализацию сеттера с проверкой, чтобы увидеть, что свойство действительно изменилось:

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

Тест 2 был очень похож, с добавлением функции, позволяющей событию отслеживать старое значение и новое значение. Поскольку эти функции должны были быть неявными в моем новом базовом методе установки, я хотел посмотреть, какая часть новых издержек была вызвана этой функцией:

    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, который все ViewModels наследуют, лежит реализация, управляющая новой функцией. Я убрал обработку ошибок, поэтому суть функции ясна:

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

Лично мне нравится использовать Microsoft PRISM NotificationObject по этой причине, и я бы предположил, что их код разумно оптимизирован, так как он создан Microsoft.

Это позволяет мне использовать такой код, как 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;
    }
}

Если вы обеспокоены тем, что решение lambda-expression-tree может быть слишком медленным, то профилируйте его и выясните. Я подозреваю, что время, потраченное на взламывание дерева выражений, будет немного меньше, чем время, которое пользовательский интерфейс будет тратить на обновление в ответ.

Если вы обнаружите, что это слишком медленно, и вам нужно использовать литеральные строки для соответствия вашим критериям производительности, то вот один подход, который я видел:

Создайте базовый класс, который реализует INotifyPropertyChangedи дать ему RaisePropertyChanged метод. Этот метод проверяет, является ли событие пустым, создает PropertyChangedEventArgsи запускает мероприятие - все как обычно.

Но метод также содержит некоторую дополнительную диагностику - он выполняет рефлексию, чтобы убедиться, что у класса действительно есть свойство с этим именем. Если свойство не существует, оно генерирует исключение. Если свойство существует, оно запоминает этот результат (например, добавляя имя свойства в статический HashSet<string>), поэтому он не должен делать проверку Reflection снова.

И все: ваши автоматизированные тесты начнут давать сбои, как только вы переименуете свойство, но не сможете обновить волшебную строку. (Я предполагаю, что у вас есть автоматические тесты для ваших ViewModels, так как это основная причина использовать MVVM.)

Если вы не хотите сбоить в производственной среде, вы можете поместить дополнительный диагностический код внутри #if DEBUG,

Одно простое решение - просто предварительно обработать все файлы перед компиляцией, обнаружить OnPropertyChanged вызовы, которые определены в блоках set { ... }, определяют имя свойства и соответственно фиксируют параметр name.

Вы можете сделать это, используя специальный инструмент (это было бы моей рекомендацией), или использовать настоящий синтаксический анализатор C# (или VB.NET) (например, те, которые можно найти здесь: Parser для C#).

Я думаю, что это разумный способ сделать это. Конечно, он не очень элегантный и не умный, но он не оказывает никакого влияния на время выполнения и соответствует правилам 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
}

На самом деле мы обсуждали это и для наших проектов и много говорили о плюсах и минусах. В конце концов, мы решили сохранить обычный метод, но использовали поле для него.

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;
        }
    }
}
Другие вопросы по тегам