Внедрение NotifyPropertyChanged без магических строк
Возможный дубликат:
typeafe 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);
}
Рассмотрим приложение, такое как электронная таблица, где, когда изменяется параметр, приблизительно 100 значений пересчитываются и обновляются в пользовательском интерфейсе в режиме реального времени. Делает это изменение настолько дорогостоящим, что это повлияет на отзывчивость пользовательского интерфейса? Я даже не могу оправдать тестирование этого изменения прямо сейчас, потому что потребуется около 2 дней обновления свойств в разных проектах и классах.
Ответы
Ответ 1
Я проверил тщательный тест NotifyPropertyChanged, чтобы установить влияние перехода на лямбда-выражения.
Вот мои результаты:
![enter image description here]()
Как вы можете видеть, использование выражения лямбда примерно в 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);
}
Bonus
Если кому-то интересно, 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;
}
}
Ответ 2
Лично мне нравится использовать 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;
}
}
Ответ 3
Если вы обеспокоены тем, что решение лямбда-выражения-дерева может быть слишком медленным, тогда выполните его анализ и выясните. Я подозреваю, что время, затрачиваемое на вскрытие дерева выражений, будет немного меньше времени, в течение которого пользовательский интерфейс будет проводить обновление в ответ.
Если вы обнаружите, что он слишком медленный, и вам нужно использовать литеральные строки для соответствия вашим критериям эффективности, то здесь один подход, который я видел:
Создайте базовый класс, реализующий INotifyPropertyChanged
, и дайте ему метод RaisePropertyChanged
. Этот метод проверяет, является ли событие нулевым, создает PropertyChangedEventArgs
и запускает событие - все обычные вещи.
Но метод также содержит некоторую дополнительную диагностику - он делает некоторое отражение, чтобы убедиться, что класс действительно имеет свойство с этим именем. Если свойство не существует, оно выдает исключение. Если свойство существует, то оно запоминает этот результат (например, добавляя имя свойства к статическому HashSet<string>
), поэтому ему не нужно снова выполнять проверку Reflection.
И вот вы: автоматические тесты начнут сбой, как только вы переименуете свойство, но не сможете обновить магическую строку. (Я предполагаю, что у вас есть автоматические тесты для ваших ViewModels, поскольку это основная причина использования MVVM.)
Если вы не хотите, чтобы вы так шумно работали, вы можете добавить дополнительный диагностический код внутри #if DEBUG
.
Ответ 4
На самом деле мы обсуждали это также для наших проектов и много говорили о плюсах и минусах. В конце концов, мы решили сохранить обычный метод, но использовали поле для него.
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;
}
}
}
Ответ 5
Одно простое решение состоит в том, чтобы просто предварительно обработать все файлы перед компиляцией, обнаружить вызовы OnPropertyChanged
, которые определены в блоках set {...}, определить имя свойства и исправить параметр имени соответственно.
Вы можете сделать это с помощью специального инструмента (это будет моя рекомендация) или использовать реальный синтаксический анализатор С# (или VB.NET) (например, те, которые можно найти здесь: Parser для С#).
Я думаю, что это разумный способ сделать это. Конечно, он не очень элегантный и умный, но он имеет нулевое влияние во время выполнения и следует правилам 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
}