Комплексные расширения / справочная реализация набора тестов
Я считаю, что интерфейс INotifyPropertyChanged является одним из наиболее важных интерфейсов, которые предоставляет платформа .NET Framework. Хотя этот интерфейс задает только одно событие, простота реализации еще не означает правильность. Я видел разные подходы к реализации того интерфейса, и каждый из них имел свои собственные недостатки.
Здесь мне бы хотелось обсудить некоторые из виденных мною подходов. Давайте начнем с того, который встречался мне наиболее часто.
Подход 1. Распространенная реализация
internal class Person1 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string name;
public string Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
OnPropertyChanged("Name");
}
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Характеристики
(+) Легко реализовать и применять.
(+) Событие вызывается только при изменении имени свойства. Это помогает избежать переполнения стека, которое возникает, когда свойства зависят друг от друга.
(-) Имя свойства задается как строковая константа в коде. Это не сохраняет рефакторинг, и это способствует возникновению ошибок, поскольку компилятор не может проверить правильность строки имени свойства.
(-) Если свойство изменяется часто, и вопросы производительности имеют большое значение, то может оказаться проблемой то, что каждое изменение свойства создает новый объект EventArgs.
(-) Метод OnPropertyChanged не соответствует рекомендациям по разработке Майкрософт (https://msdn.microsoft.com/ru-ru/library/ms229011.aspx), поскольку этот метод должен иметь аргумент типа PropertyChangedEventArgs вместо типа string. Одну из причин для этого правила можно увидеть в подходе 2.
Подход 2. Кэшированный объект PropertyChangedEventArgs
internal class Person2 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private static readonly PropertyChangedEventArgs NameEventArgs = new PropertyChangedEventArgs("Name");
private string name;
public string Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
OnPropertyChanged(NameEventArgs);
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) { PropertyChanged(this, e); }
}
}
Характеристики
(+) Событие вызывается только при изменении имени свойства. Это помогает избежать переполнения стека, которое возникает, когда свойства зависят друг от друга.
(+) PropertyChangedEventArgs создаются один раз для класса, а не для каждого изменения свойства. Это улучшает производительность.
(+) Метод OnPropertyChanged соответствует рекомендациям по разработке Майкрософт (https://msdn.microsoft.com/ru-ru/library/ms229011.aspx). Это позволяет нам кэшировать PropertyChangedEventArgs для повторного использования при каждом изменении свойства. Более того, подкласс сможет передавать производный тип PropertyChangedEventArgs посредством этого метода. Например, этот производный класс может включать старое и новое значение операции изменения свойства.
(-) Имя свойства задается как строковая константа в коде. Это не сохраняет рефакторинг, и это способствует возникновению ошибок, поскольку компилятор не может проверить правильность строки имени свойства.
(-) Каждому свойству необходимо статическое поле для PropertyChangedEventArgs.
Подход 3. Проверка PropertyName во время выполнения
internal class Person3 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string name;
public string Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
OnPropertyChanged("Name");
}
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
CheckPropertyName(propertyName);
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
[Conditional("DEBUG")]
private void CheckPropertyName(string propertyName)
{
PropertyDescriptor propertyDescriptor = TypeDescriptor.GetProperties(this)[propertyName];
if (propertyDescriptor == null)
{
throw new InvalidOperationException(string.Format(null,
"The property with the propertyName '{0}' doesn't exist.", propertyName));
}
}
}
Характеристики
(+) Событие вызывается только при изменении имени свойства. Это помогает избежать переполнения стека, которое возникает, когда свойства зависят друг от друга.
(+) Метод OnPropertyChanged выполняет проверки допустимости переданного имени свойства. Это очень помогает найти неправильные имена свойств, которые часто возникают в результате опечаток или рефакторинга кода. Однако эта проверка потребляет большой объем производительности и поэтому выполняется только в режиме отладки.
(-) Если свойство изменяется часто, и вопросы производительности имеют большое значение, то может оказаться проблемой то, что каждое изменение свойства создает новый объект EventArgs.
(-) Метод OnPropertyChanged не соответствует рекомендациям по разработке Майкрософт (https://msdn.microsoft.com/ru-ru/library/ms229011.aspx), поскольку этот метод должен иметь аргумент типа PropertyChangedEventArgs вместо типа string. Одну из причин для этого правила можно увидеть в подходе 2.
Подход 4. Извлечение имени свойства с помощью лямбда-выражения
internal class Person4 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private static readonly string NamePropertyName = TypeManager.GetProperty<Person4>(x => x.Name).Name;
private string name;
public string Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
OnPropertyChanged(NamePropertyName);
}
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Примечание. Реализацию метода TypeManager.GetProperty можно увидеть в загрузке кода.
Характеристики
(+) Событие вызывается только при изменении имени свойства. Это помогает избежать переполнения стека, которое возникает, когда свойства зависят друг от друга.
(+) Метод TypeManager.GetProperty извлекает имя свойства с помощью лямбда-выражения. Лямбда-выражение типобезопасно.
(-) Для извлечения свойства лямбда-выражение использует отражение внутренним образом. Это увеличивает рабочее множество (потребление памяти) приложения и замедляется, когда доступ к классу осуществляется впервые.
(-) Использование статического метода TypeManager.GetProperty не прямолинейно.
(-) Если свойство изменяется часто, и вопросы производительности имеют большое значение, то может оказаться проблемой то, что каждое изменение свойства создает новый объект EventArgs.
(-) Метод OnPropertyChanged не соответствует рекомендациям по разработке Майкрософт (https://msdn.microsoft.com/ru-ru/library/ms229011.aspx), поскольку этот метод должен иметь аргумент типа PropertyChangedEventArgs вместо типа string. Одну из причин для этого правила можно увидеть в подходе 2.
Подход 5. Отказ от использования класса StackTrace
// DO NOT USE THIS!
protected void OnPropertyChanged()
{
StackTrace s = new StackTrace(1, false);
string propertyName = s.GetFrame(0).GetMethod().Name.Substring(4);
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Я также видел реализации, использующие класс StackTrace для извлечения имени свойства. К сожалению, это не работает правильно по разным причинам.
- Реализация StackTrace полагается на символы отладки. По умолчанию отладочные построения включают символы отладки, в то время как рабочие построения — нет. Следовательно, приложение работает в режиме отладки, но больше не работает в рабочем режиме.
- Класс StackTrace не предназначен для частых вызовов. Если его использовать для реализации измененного свойства, то производительность может быть низкой.
Существует дополнительный подход, который я использую, и который является комбинацией 2 и 4 подходов — кэширование объекта PropertyChangedEventArgs в статической переменной. Используется вспомогательная функция, которая применяет синтаксис выражений для возврата созданного объекта EventArgs. Затем кэшированный объект EventArgs используется для уведомлений об изменении.
Пример использования:
class Person6 : INotifyPropertyChanged
{
static readonly PropertyChangedEventArg _nameChangedArgs = ObservableHelper.CreateArgs<Person6>(x => x.Name);
string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(_nameChangedArgs);
}
}
}
protected void OnPropertyChanged(PropertyChangedEventArgs propertyChangeArgs)
{
var changeEvent = PropertyChanged;
if (changeEvent != null)
{
changeEvent(this, propertyChangeArgs);
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Это, конечно, приводит к снижению производительности при первом использовании этого типа. Лично я не вижу в этом проблему — это однократное снижение. И этот подход имеет преимущество в том, что он немного быстрее при уведомлении об изменении.
С уважением,
Фил
ИСПРАВЛЕНИЕ. Между прочим, в базовом классе, который у меня был для реализации INotifyPropertyChanged, я *не* предоставлял метод "OnPropertyChanged", который принимает строковый аргумент. Причина в том, что я активно хотел отговорить пользователей от передачи в жестко закодированных строках и подтолкнуть их к использованию моего предоставленного вспомогательного метода и синтаксиса выражений, чтобы их код был безопасен в плане рефакторинга и запутанности. В конечном счете ничто не может помешать им ввести объект EventArgs, если они этого хотят, но я объясняю в XML-комментариях метода OnPropertyChanged, что им следует использовать вспомогательный метод CreateArgs и синтаксис выражений.
Имеется дополнительный метод, предоставляемый в базовом классе с именем "OnAllPropertiesChanged()", который передает событие изменения свойства с помощью кэшированного объекта EventArgs измененного свойства, созданного с помощью string.empty. Это поддерживает соглашение для INotifyPropertyChanged, когда значение NULL или пустая строка может использоваться в аргументах изменения свойств, чтобы указать, что получатель должен предполагать изменение всех свойств (и если привязка повторно запрашивает у этих свойств их текущие значения).
Далее приводится типобезопасная реализация с использованием деревьев выражений.
public void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
{
var lambda = (LambdaExpression)property;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = (UnaryExpression)lambda.Body;
memberExpression = (MemberExpression)unaryExpression.Operand;
}
else memberExpression = (MemberExpression)lambda.Body;
NotifyOfPropertyChange(memberExpression.Member.Name);
}
Она может использоваться следующим образом.
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
NotifyOfPropertyChange(() => Name);
}
}
}