Автоматически INotifyPropertyChanged
Есть ли способ автоматически получать уведомления об изменениях свойств в классе без необходимости писать OnPropertyChanged в каждом сеттере? (У меня есть сотни свойств, которые я хочу знать, если они изменились).
Антон предлагает динамические прокси. Я фактически использовал библиотеку "Castle" для чего-то похожего в прошлом, и хотя это действительно уменьшает объем кода, который мне пришлось написать, он прибавил около 30 секунд ко времени запуска моей программы (ymmv) - потому что это решение во время выполнения.
Мне интересно, есть ли решение времени компиляции, может быть, с использованием атрибутов времени компиляции...
Slashene и TcK дают предложения, которые генерируют повторяющийся код - к сожалению, не все мои свойства являются простым случаем m_Value = value - у многих из них есть собственный код в установщиках, поэтому код резака cookie из фрагментов и xml на самом деле неосуществим для мой проект тоже.
13 ответов
РЕДАКТИРОВАТЬ: Автор NotifyPropertyWeaver устарел инструмент в пользу более общего Fody. ( Руководство по миграции для людей, переходящих от ткача к фоде.)
Очень удобный инструмент, который я использовал для своих проектов, это Notify Property Weaver Fody.
Он устанавливается как шаг сборки в ваших проектах и во время компиляции внедряет код, который вызывает PropertyChanged
событие.
Повышение свойств PropertyChanged выполняется путем установки на них специальных атрибутов:
[ImplementPropertyChanged]
public string MyProperty { get; set; }
В качестве бонуса вы также можете указать отношения для свойств, которые зависят от других свойств
[ImplementPropertyChanged]
public double Radius { get; set; }
[DependsOn("Radius")]
public double Area
{
get { return Radius * Radius * Math.PI; }
}
Оператор nameof был реализован в C# 6.0 с.NET 4.6 и VS2015 в июле 2015 года. Следующее все еще действует для C# < 6.0
Мы используем код ниже (с http://www.ingebrigtsen.info/post/2008/12/11/INotifyPropertyChanged-revisited.aspx). Работает отлично:)
public static class NotificationExtensions
{
#region Delegates
/// <summary>
/// A property changed handler without the property name.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sender">The object that raised the event.</param>
public delegate void PropertyChangedHandler<TSender>(TSender sender);
#endregion
/// <summary>
/// Notifies listeners about a change.
/// </summary>
/// <param name="EventHandler">The event to raise.</param>
/// <param name="Property">The property that changed.</param>
public static void Notify(this PropertyChangedEventHandler EventHandler, Expression<Func<object>> Property)
{
// Check for null
if (EventHandler == null)
return;
// Get property name
var lambda = Property 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;
}
ConstantExpression constantExpression;
if (memberExpression.Expression is UnaryExpression)
{
var unaryExpression = memberExpression.Expression as UnaryExpression;
constantExpression = unaryExpression.Operand as ConstantExpression;
}
else
{
constantExpression = memberExpression.Expression as ConstantExpression;
}
var propertyInfo = memberExpression.Member as PropertyInfo;
// Invoke event
foreach (Delegate del in EventHandler.GetInvocationList())
{
del.DynamicInvoke(new[]
{
constantExpression.Value, new PropertyChangedEventArgs(propertyInfo.Name)
});
}
}
/// <summary>
/// Subscribe to changes in an object implementing INotifiyPropertyChanged.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="ObjectThatNotifies">The object you are interested in.</param>
/// <param name="Property">The property you are interested in.</param>
/// <param name="Handler">The delegate that will handle the event.</param>
public static void SubscribeToChange<T>(this T ObjectThatNotifies, Expression<Func<object>> Property, PropertyChangedHandler<T> Handler) where T : INotifyPropertyChanged
{
// Add a new PropertyChangedEventHandler
ObjectThatNotifies.PropertyChanged += (s, e) =>
{
// Get name of Property
var lambda = Property 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;
// Notify handler if PropertyName is the one we were interested in
if (e.PropertyName.Equals(propertyInfo.Name))
{
Handler(ObjectThatNotifies);
}
};
}
}
Используется, например, таким образом:
public class Employee : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _firstName;
public string FirstName
{
get { return this._firstName; }
set
{
this._firstName = value;
this.PropertyChanged.Notify(()=>this.FirstName);
}
}
}
private void firstName_PropertyChanged(Employee sender)
{
Console.WriteLine(sender.FirstName);
}
employee = new Employee();
employee.SubscribeToChange(() => employee.FirstName, firstName_PropertyChanged);
Некоторые синтаксические ошибки в примере могут существовать. Не проверял это. Но у тебя должна быть хотя бы концепция:)
РЕДАКТИРОВАТЬ: Теперь я вижу, что вы, возможно, хотели еще меньше работы, но да... материал выше, по крайней мере, делает это намного проще. И вы предотвращаете все страшные проблемы со ссылками на свойства, используя строки.
Framework 4.5 предоставляет нам CallerMemberNameAttribute
, что делает ненужной передачу имени свойства в виде строки:
private string m_myProperty;
public string MyProperty
{
get { return m_myProperty; }
set
{
m_myProperty = value;
OnPropertyChanged();
}
}
private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed")
{
// ... do stuff here ...
}
Похоже на решение Svish, просто заменяя лямбда-класс на скучную функциональность фреймворка;-)
Если вы работаете на Framework 4.0 с установленным KB2468871, вы можете установить пакет совместимости Microsoft BCL через nuget, который также предоставляет этот атрибут.
Реализуйте безопасный тип INotifyPropertyChanged
: Смотрите здесь
Затем создайте свой собственный фрагмент кода:
private $Type$ _$PropertyName$;
public $Type$ $PropertyName$
{
get
{
return _$PropertyName$;
}
set
{
if(value != _$PropertyName$)
{
_$PropertyName$ = value;
OnPropertyChanged(o => o.$PropertyName$);
}
}
}
С дизайнером фрагмента кода, и вы сделали! Простой и безопасный способ покупки вашего INotifyPropertyChanged.
Вы можете иметь метод расширения для вашего делегата PropertyChanged и использовать его следующим образом:
public string Name
{
get { return name; }
set
{
name = value;
PropertyChanged.Raise(() => Name);
}
}
Подписка на конкретное изменение свойства:
var obj = new Employee();
var handler = obj.SubscribeToPropertyChanged(
o => o.FirstName,
o => Console.WriteLine("FirstName is now '{0}'", o.FirstName));
obj.FirstName = "abc";
// Unsubscribe when required
obj.PropertyChanged -= handler;
Метод расширения может определить имя отправителя и имя свойства, просто проверив дерево лямбда-выражений без существенного влияния на производительность:
public static class PropertyChangedExtensions
{
public static void Raise<TProperty>(
this PropertyChangedEventHandler handler, Expression<Func<TProperty>> property)
{
if (handler == null)
return;
var memberExpr = (MemberExpression)property.Body;
var propertyName = memberExpr.Member.Name;
var sender = ((ConstantExpression)memberExpr.Expression).Value;
handler.Invoke(sender, new PropertyChangedEventArgs(propertyName));
}
public static PropertyChangedEventHandler SubscribeToPropertyChanged<T, TProperty>(
this T obj, Expression<Func<T, TProperty>> property, Action<T> handler)
where T : INotifyPropertyChanged
{
if (handler == null)
return null;
var memberExpr = (MemberExpression)property.Body;
var propertyName = memberExpr.Member.Name;
PropertyChangedEventHandler subscription = (sender, eventArgs) =>
{
if (propertyName == eventArgs.PropertyName)
handler(obj);
};
obj.PropertyChanged += subscription;
return subscription;
}
}
Если PropertyChanged
Событие объявляется в базовом типе, тогда оно не будет отображаться как поле делегата в производных классах. В этом случае обходной путь должен объявить защищенное поле типа PropertyChangedEventHandler
и явно реализовать событие add
а также remove
аксессоры:
public class Base : INotifyPropertyChanged
{
protected PropertyChangedEventHandler propertyChanged;
public event PropertyChangedEventHandler PropertyChanged
{
add { propertyChanged += value; }
remove { propertyChanged -= value; }
}
}
public class Derived : Base
{
string name;
public string Name
{
get { return name; }
set
{
name = value;
propertyChanged.Raise(() => Name);
}
}
}
Вы можете посмотреть на Аспектно-ориентированное программирование в целом
Frameworks => вы можете посмотреть на linfu
Я не знаю стандартного способа, но я знаю два обходных пути:
1) PostSharp может сделать это за вас после компиляции. Это очень полезно, но это займет некоторое время на каждой сборке.
2) Пользовательский инструмент в Visual Studio. Вы можете комбинировать это с "частичным классом". Затем вы можете создать собственный инструмент для вашего XML и сгенерировать исходный код из XML.
Например, этот XML:
<type scope="public" type="class" name="MyClass">
<property scope="public" type="string" modifier="virtual" name="Text" notify="true" />
</type>
может быть источником для этого кода:
public partial class MyClass {
private string _text;
public virtual string Text {
get { return this._Text; }
set {
this.OnPropertyChanging( "Text" );
this._Text = value;
this.OnPropertyChanged( "Text" );
}
}
}
Я только что нашел ActiveSharp - автоматический INotifyPropertyChanged, я еще не использовал его, но выглядит хорошо.
Цитировать с его веб-сайта...
Отправлять уведомления об изменении свойства без указания имени свойства в виде строки.
Вместо этого напишите такие свойства:
public int Foo
{
get { return _foo; }
set { SetValue(ref _foo, value); } // <-- no property name here
}
Обратите внимание, что нет необходимости включать имя свойства в виде строки. ActiveSharp надежно и правильно вычисляет это для себя. Он работает на основе того факта, что ваша реализация свойства передает поле поддержки (_foo) по ссылке. (ActiveSharp использует этот вызов "по ссылке", чтобы определить, какое поле поддержки было передано, и из поля оно идентифицирует свойство).
Вы могли бы взглянуть на Castle или Spring.NET и реализовать функциональность перехватчика?
Не существует единой реализации измененного свойства, которая могла бы обрабатывать все способы, которыми люди хотят ее использовать. Лучше всего создать вспомогательный класс, чтобы сделать работу за вас. Вот пример, который я использую.
/// <summary>
/// Helper Class that automates most of the actions required to implement INotifyPropertyChanged
/// </summary>
public static class HPropertyChanged
{
private static Dictionary<string, PropertyChangedEventArgs> argslookup = new Dictionary<string, PropertyChangedEventArgs>();
public static string ThisPropertyName([CallerMemberName]string name = "")
{
return name;
}
public static string GetPropertyName<T>(Expression<Func<T>> exp)
{
string rtn = "";
MemberExpression mex = exp.Body as MemberExpression;
if(mex!=null)
rtn = mex.Member.Name;
return rtn;
}
public static void SetValue<T>(ref T target, T newVal, object sender, PropertyChangedEventHandler handler, params string[] changed)
{
if (!target.Equals(newVal))
{
target = newVal;
PropertyChanged(sender, handler, changed);
}
}
public static void SetValue<T>(ref T target, T newVal, Action<PropertyChangedEventArgs> handler, params string[] changed)
{
if (!target.Equals(newVal))
{
target = newVal;
foreach (var item in changed)
{
handler(GetArg(item));
}
}
}
public static void PropertyChanged(object sender,PropertyChangedEventHandler handler,params string[] changed)
{
if (handler!=null)
{
foreach (var prop in changed)
{
handler(sender, GetArg(prop));
}
}
}
public static PropertyChangedEventArgs GetArg(string name)
{
if (!argslookup.ContainsKey(name)) argslookup.Add(name, new PropertyChangedEventArgs(name));
return argslookup[name];
}
}
изменить: было предложено, чтобы я перешел от вспомогательного класса к оболочке значения, и с тех пор я использую этот, и я считаю, что он работает довольно хорошо
public class NotifyValue<T>
{
public static implicit operator T(NotifyValue<T> item)
{
return item.Value;
}
public NotifyValue(object parent, T value = default(T), PropertyChangingEventHandler changing = null, PropertyChangedEventHandler changed = null, params object[] dependenies)
{
_parent = parent;
_propertyChanged = changed;
_propertyChanging = changing;
if (_propertyChanged != null)
{
_propertyChangedArg =
dependenies.OfType<PropertyChangedEventArgs>()
.Union(
from d in dependenies.OfType<string>()
select new PropertyChangedEventArgs(d)
);
}
if (_propertyChanging != null)
{
_propertyChangingArg =
dependenies.OfType<PropertyChangingEventArgs>()
.Union(
from d in dependenies.OfType<string>()
select new PropertyChangingEventArgs(d)
);
}
_PostChangeActions = dependenies.OfType<Action>();
}
private T _Value;
public T Value
{
get { return _Value; }
set
{
SetValue(value);
}
}
public bool SetValue(T value)
{
if (!EqualityComparer<T>.Default.Equals(_Value, value))
{
OnPropertyChnaging();
_Value = value;
OnPropertyChnaged();
foreach (var action in _PostChangeActions)
{
action();
}
return true;
}
else
return false;
}
private void OnPropertyChnaged()
{
var handler = _propertyChanged;
if (handler != null)
{
foreach (var arg in _propertyChangedArg)
{
handler(_parent, arg);
}
}
}
private void OnPropertyChnaging()
{
var handler = _propertyChanging;
if(handler!=null)
{
foreach (var arg in _propertyChangingArg)
{
handler(_parent, arg);
}
}
}
private object _parent;
private PropertyChangedEventHandler _propertyChanged;
private PropertyChangingEventHandler _propertyChanging;
private IEnumerable<PropertyChangedEventArgs> _propertyChangedArg;
private IEnumerable<PropertyChangingEventArgs> _propertyChangingArg;
private IEnumerable<Action> _PostChangeActions;
}
пример использования
private NotifyValue<int> _val;
public const string ValueProperty = "Value";
public int Value
{
get { return _val.Value; }
set { _val.Value = value; }
}
тогда в конструкторе вы делаете
_val = new NotifyValue<int>(this,0,PropertyChanged,PropertyChanging,ValueProperty );
Улучшение для вызова мероприятия в детских классах:
Вызывается благодаря: this.NotifyPropertyChange(() => PageIndex);
Добавьте это в класс NotificationExtensions:
/// <summary>
/// <para>Lève l'évènement de changement de valeur sur l'objet <paramref name="sender"/>
/// pour la propriété utilisée dans la lambda <paramref name="property"/>.</para>
/// </summary>
/// <param name="sender">L'objet portant la propriété et l'évènement.</param>
/// <param name="property">Une expression lambda utilisant la propriété subissant la modification.</param>
public static void NotifyPropertyChange(this INotifyPropertyChanged sender, Expression<Func<Object>> property)
{
if (sender == null)
return;
// Récupère le nom de la propriété utilisée dans la lambda en argument
LambdaExpression lambda = property as LambdaExpression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
UnaryExpression unaryExpression = lambda.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
{
memberExpression = lambda.Body as MemberExpression;
}
ConstantExpression constantExpression = memberExpression.Expression as ConstantExpression;
PropertyInfo propertyInfo = memberExpression.Member as PropertyInfo;
// il faut remonter la hierarchie, car meme public, un event n est pas visible dans les enfants
FieldInfo eventField;
Type baseType = sender.GetType();
do
{
eventField = baseType.GetField(INotifyPropertyChangedEventFieldName, BindingFlags.Instance | BindingFlags.NonPublic);
baseType = baseType.BaseType;
} while (eventField == null);
// on a trouvé l'event, on peut invoquer tt les delegates liés
MulticastDelegate eventDelegate = eventField.GetValue(sender) as MulticastDelegate;
if (eventDelegate == null) return; // l'event n'est bindé à aucun delegate
foreach (Delegate handler in eventDelegate.GetInvocationList())
{
handler.Method.Invoke(handler.Target, new Object[] { sender, new PropertyChangedEventArgs(propertyInfo.Name) });
}
}
Просто чтобы сделать реализацию быстрее, вы можете использовать фрагмент
От http://aaron-hoffman.blogspot.it/2010/09/visual-studio-code-snippet-for-notify.html
В классах ViewModel проектов, следующих шаблону MV-VM, часто необходимо вызывать событие "PropertyChanged" (для помощи в реализации интерфейса INotifyPropertyChanged) из установщика свойства. Это утомительная задача, которая, надеюсь, когда-нибудь будет решена с помощью компилятора в качестве службы...
Ядром сниппета (за что автором, а не мной) является следующее
<Code Language= "csharp ">
<![CDATA[public $type$ $property$
{
get { return _$property$; }
set
{
if (_$property$ != value)
{
_$property$ = value;
OnPropertyChanged($property$PropertyName);
}
}
}
private $type$ _$property$;
public const string $property$PropertyName = "$property$";$end$]]>
</Code>
Просто используйте этот атрибут над вашей автоматической декларацией свойства
[NotifyParentProperty(true)]
public object YourProperty { get; set; }