Реализация 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;
}
}
}