Как лучше всего указать имя свойства при использовании INotifyPropertyChanged?
В большинстве примеров имя свойства жестко кодируется как аргумент в событии PropertyChanged. Я думал об использовании MethodBase.GetCurrentMethod.Name.Substring (4), но меня немного беспокоят накладные расходы на отражение.





Накладные расходы на отражение здесь в значительной степени избыточны, особенно потому, что INotifyPropertyChanged называется много. Лучше просто жестко запрограммировать значение, если можете.
Если вас не беспокоит производительность, я бы посмотрел на различные подходы, упомянутые ниже, и выбрал бы тот, который требует наименьшего количества кодирования. Если бы вы могли что-то сделать, чтобы полностью устранить необходимость в явном вызове, это было бы лучше всего (например, АОП).
Да, я вижу использование и простоту функции, которую вы предлагаете, но, учитывая текущие затраты из-за отражения, да, это плохая идея. В этом сценарии я использую правильно добавленный Фрагмент кода, чтобы использовать время и ошибка при записи свойства со всеми срабатываниями события Notifyproperty.
Кроме того, мы обнаружили проблему, из-за которой получение имени метода работало по-разному в сборках Debug и Release:
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/244d3f24-4cc4-4925-aebe-85f55b39ec92
(Код, который мы использовали, не совсем отражал то, что вы предложили, но он убедил нас, что жесткое кодирование имени свойства было самым быстрым и надежным решением.)
Я проделал что-то подобное однажды в качестве эксперимента, по памяти все работало нормально, и я устранил необходимость жестко кодировать все имена свойств в строках. Если вы создаете серверное приложение большого объема, на рабочем столе вы, вероятно, никогда не заметите разницы с производительностью.
protected void OnPropertyChanged()
{
OnPropertyChanged(PropertyName);
}
protected string PropertyName
{
get
{
MethodBase mb = new StackFrame(1).GetMethod();
string name = mb.Name;
if (mb.Name.IndexOf("get_") > -1)
name = mb.Name.Replace("get_", "");
if (mb.Name.IndexOf("set_") > -1)
name = mb.Name.Replace("set_", "");
return name;
}
}
Мне нравится творческий подход к этому. Наверное, не стоит использовать на практике. Но концепция интересная.
Проблема с методом, основанным на отражении, заключается в том, что он довольно дорогой и не очень быстрый. Конечно, он гораздо более гибкий и менее уязвимый для рефакторинга.
Однако это действительно может снизить производительность, особенно когда что-то вызывается часто. Метод stackframe также (я считаю) имеет проблемы в CAS (например, ограниченные уровни доверия, такие как XBAP). Лучше всего это жестко закодировать.
Если вы ищете быстрое и гибкое уведомление о свойствах в WPF, есть решение - используйте DependencyObject :) Вот для чего он был разработан. Если вы не хотите принимать зависимость или беспокоиться о проблемах сродства потоков, переместите имя свойства в константу и бум! Ваше хорошее.
Не забывайте одну вещь: событие PropertyChanged в основном используется компонентами, которые будут использовать использовать отражение для получения значения указанного свойства.
Самый очевидный пример - привязка данных.
Когда вы запускаете событие PropertyChanged, передавая имя свойства в качестве параметра, вы должны знать, что подписчик этого события, вероятно, будет использовать отражение, вызывая, например, GetProperty (по крайней мере, в первый раз, если он использует кеш PropertyInfo), а затем GetValue. Этот последний вызов является динамическим вызовом (MethodInfo.Invoke) метода получения свойств, который стоит дороже, чем GetProperty, который запрашивает только метаданные. (Обратите внимание, что привязка данных зависит от всей вещи TypeDescriptor, но реализация по умолчанию использует отражение.)
Итак, конечно, использование имен свойств жесткого кода при запуске PropertyChanged более эффективно, чем использование отражения для динамического получения имени свойства, но ИМХО, важно сбалансировать свои мысли. В некоторых случаях накладные расходы на производительность не так критичны, и вы могли бы извлечь выгоду из какого-то строго типизированного механизма запуска событий.
Вот что я иногда использую в C# 3.0, когда производительность не будет проблемой:
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return this.name; }
set
{
this.name = value;
FirePropertyChanged(p => p.Name);
}
}
private void FirePropertyChanged<TValue>(Expression<Func<Person, TValue>> propertySelector)
{
if (PropertyChanged == null)
return;
var memberExpression = propertySelector.Body as MemberExpression;
if (memberExpression == null)
return;
PropertyChanged(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Обратите внимание на использование дерева выражений для получения имени свойства и использование лямбда-выражения в качестве Expression:
FirePropertyChanged(p => p.Name);
Это кажется излишним для проблемы изменения собственности. Несмотря на то, что он поддерживает все в рефакторируемом коде, вы получаете намного больше кода, который нужно поддерживать, и этот код менее читабелен, чем исходная концепция OnPropertyChanged («Name») или аналогичного метода.
+1 за объяснение и подтверждение того, что «подписчик этого события, вероятно, будет использовать отражение, вызвав, например, GetProperty». Мне было интересно, что произошло после того, как объект данных сделал OnPropertyChanged («Имя»).
Очень круто. Как вы думаете, есть ли способ полностью избежать использования строк?
@strongriley: В представленном здесь решении не используется никакая строка. Так что я не уверен, что понимаю, что вы имеете в виду ...
Я имею в виду, когда вы потребляете событие. Вместо использования INotifyPropertyChanged, можно ли создать интерфейс, в котором EventArgs не использовал бы строковый литерал на первом месте?
Обратите внимание, что Microsoft никогда не показывает пример решения для отражения. В .NET 4.5 теперь можно использовать новый атрибут [CallerMemberName] msdn.microsoft.com/en-us/library/…
Есть ли способ, используя Reflection, что вам вообще не пришлось бы вводить часть Name? В приведенном выше примере вам нужно ввести даже больше, чем просто "Name", вам нужно ввести p => p.Name - я думаю, это дает вам строгую типизацию? ... но это, конечно, не избавит вас от каких-либо проблем в том, что касается того, сколько вам нужно печатать.
Роман:
Я бы сказал, что вам даже не понадобится параметр "Person" - соответственно, подойдет полностью общий фрагмент, подобный приведенному ниже:
private int age;
public int Age
{
get { return age; }
set
{
age = value;
OnPropertyChanged(() => Age);
}
}
private void OnPropertyChanged<T>(Expression<Func<T>> exp)
{
//the cast will always succeed
MemberExpression memberExpression = (MemberExpression) exp.Body;
string propertyName = memberExpression.Member.Name;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
... однако я предпочитаю придерживаться строковых параметров с условной проверкой в сборках отладки. Джош Смит опубликовал хороший пример:
Базовый класс, реализующий INotifyPropertyChanged
Ваше здоровье :) Филипп
Я голосую за строки с условной проверкой. Это просто и работает. В конце концов, ни одно из этих решений отражения не решает проблему, которую я хотел бы решить - проверку времени компиляции уведомлений об изменении свойств.
Возможно, вы захотите вообще избежать INotifyPropertyChanged. Он добавляет в ваш проект ненужный бухгалтерский код. Вместо этого рассмотрите возможность использования Обновить элементы управления .NET.
Возможно, вас заинтересует обсуждение
«Лучшие практики: как правильно реализовать INotifyPropertyChanged?»
тоже.
Еще один ОЧЕНЬ ПРИЯТНЫЙ метод, о котором я могу думать, - это
Автоматическая реализация INotifyPropertyChanged с аспектами
АОП: аспектно-ориентированное программирование
Хорошая статья о codeproject: Реализация INotifyPropertyChanged в АОП
Я также считаю, что АОП - лучшее решение для такого рода проблем. Но вы знаете, что большинство разработчиков C# на самом деле не знают, что такое АОП.
Падение производительности при использовании деревьев выражений происходит из-за многократного разрешения дерева выражений.
Следующий код по-прежнему использует деревья выражений и, следовательно, имеет следующие преимущества, заключающиеся в том, что он удобен для рефакторинга и обфускации, но на самом деле он примерно на 40% быстрее (очень грубые тесты), чем обычный метод, который состоит из создания объекта PropertyChangedEventArgs для каждого уведомления об изменении. .
Это быстрее и позволяет избежать снижения производительности дерева выражений, поскольку мы кэшируем статический объект PropertyChangedEventArgs для каждого свойства.
Есть одна вещь, которую я еще не делаю - я собираюсь добавить код, который проверяет наличие отладочных сборок, чтобы имя свойства для предоставленного объекта PropertChangedEventArgs совпадает со свойством, в котором он используется - на данный момент с этим кодом оно все еще возможно, что разработчик поставит не тот объект.
Проверить это:
public class Observable<T> : INotifyPropertyChanged
where T : Observable<T>
{
public event PropertyChangedEventHandler PropertyChanged;
protected static PropertyChangedEventArgs CreateArgs(
Expression<Func<T, object>> propertyExpression)
{
var lambda = propertyExpression as LambdaExpression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = lambda.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
{
memberExpression = lambda.Body as MemberExpression;
}
var propertyInfo = memberExpression.Member as PropertyInfo;
return new PropertyChangedEventArgs(propertyInfo.Name);
}
protected void NotifyChange(PropertyChangedEventArgs args)
{
if (PropertyChanged != null)
{
PropertyChanged(this, args);
}
}
}
public class Person : Observable<Person>
{
// property change event arg objects
static PropertyChangedEventArgs _firstNameChangeArgs = CreateArgs(x => x.FirstName);
static PropertyChangedEventArgs _lastNameChangeArgs = CreateArgs(x => x.LastName);
string _firstName;
string _lastName;
public string FirstName
{
get { return _firstName; }
set
{
_firstName = value;
NotifyChange(_firstNameChangeArgs);
}
}
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
NotifyChange(_lastNameChangeArgs);
}
}
}
Обратите внимание, что на самом деле есть проблема с приведенным выше кодом - он ограничивает T непосредственным подтипом (поскольку непосредственный подтип указывает T как сам). Это означает, что вы не можете использовать CreateArgs при работе со свойствами в классе, который находится ниже по иерархии от Person. Простое исправление этого - сделать метод CreateArgs универсальным, то есть CreateArgs <T> вместо универсального класса Observable. Это немного сложнее использовать, поскольку вам нужно явно указать тип, например: static PropertyChangedEventArgs _firstNameChangeArgs = CreateArgs <Person> (x => x.FirstName);
Все, что заставляет меня сменить базовый класс, - это большой запрет. У меня точно нет возможности изменить базовый класс на что-то, что уже должно иметь другой базовый класс или имеет автоматически сгенерированный базовый класс.
Orion ... если это так, должно быть достаточно легко преобразовать приведенное выше из базового класса Observable во вспомогательный класс (например, ObservableHelper), который вы можете вызвать явно. Если вам интересно, я, вероятно, мог бы опубликовать обновление, которое будет охватывать случай использования, когда вы не можете контролировать, какой базовый класс используется, или не хотите его вводить. Хотя там, где вы управляете базовым классом, я думаю, есть преимущества в использовании базового класса; простота использования является основным.
Взгляните на это сообщение в блоге: http://khason.net/dev/inotifypropertychanged-auto-wiring-or-how-to-get-rid-of-redundant-code
Безусловно, между жестким кодом и отражением мой выбор: уведомить.
Этот пакет Visual Studio позволяет вам получить преимущества отражения (ремонтопригодность, удобочитаемость и т. д.) Без потери производительности.
Фактически, вам просто нужно реализовать INotifyPropertyChanged, и он добавляет все «уведомления» при компиляции.
Это также полностью параметризуется, если вы хотите полностью оптимизировать свой код.
Например, с notifypropertyweaver у вас будет этот код в вашем редакторе:
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string GivenNames { get; set; }
public string FamilyName { get; set; }
public string FullName
{
get
{
return string.Format("{0} {1}", GivenNames, FamilyName);
}
}
}
Вместо :
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string givenNames;
public string GivenNames
{
get { return givenNames; }
set
{
if (value != givenNames)
{
givenNames = value;
OnPropertyChanged("GivenNames");
OnPropertyChanged("FullName");
}
}
}
private string familyName;
public string FamilyName
{
get { return familyName; }
set
{
if (value != familyName)
{
familyName = value;
OnPropertyChanged("FamilyName");
OnPropertyChanged("FullName");
}
}
}
public string FullName
{
get
{
return string.Format("{0} {1}", GivenNames, FamilyName);
}
}
public virtual void OnPropertyChanged(string propertyName)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Для франкоговорящих: Améliorez la lisibilité de votre code et simpleifiez vous la vie avec notifypropertyweaver
В .NET 4.5 (C# 5.0) есть новый атрибут, называемый - CallerMemberName, он помогает избежать жестко заданных имен свойств, предотвращающих возникновение ошибок, если разработчики решат изменить имя свойства, вот пример:
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private string name;
public string Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged();
}
}
Поскольку в C# 6.0 есть ключевое слово название(), оно будет оцениваться во время компиляции, поэтому оно будет иметь производительность как жестко запрограммированное значение и будет защищено от несоответствия с уведомленным свойством.
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string info)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
}
public string SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem != value)
{
_selectedItem = value;
NotifyPropertyChanged(nameof(SelectedItem));
}
}
}
private string _selectedItem;
.NET 4.5 дает хорошее решение этой проблемы с атрибутом [CallerMemberName]. См. Пример его использования в документации MSDN msdn.microsoft.com/en-us/library/…