Automatiquement INotifyPropertyChanged
y a-t-il un moyen d'être automatiquement informé des changements de propriété dans une classe sans avoir à écrire Surpropertychanged dans chaque setter? (J'ai des centaines de propriétés que je veux savoir si elles ont changé).
Anton suggère dynamic proxies . J'ai en fait utilisé la bibliothèque" Castle " pour quelque chose de similaire dans le passé, et bien qu'elle réduise la quantité de code que j'ai dû écrire, elle a ajouté environ 30 secondes avant le démarrage de mon programme (ymmv) - parce que c'est une solution d'exécution.
je me demande s'il y a une solution de temps de compilation, peut-être en utilisant des attributs de temps de compilation...
Slashene et TcKs donnent des suggestions qui génèrent du code répétitif - malheureusement, toutes mes propriétés ne sont pas un simple cas de m_value = value - beaucoup d'entre elles ont du code personnalisé dans les setters, donc le code de type cookie-cutter des snippets et xml ne sont pas vraiment faisable pour mon projet.
13 réponses
EDIT: l'auteur de NotifyPropertyWeaver a déprécié l'outil en faveur de la plus générale Fody . (Un guide de migration pour les personnes se déplaçant à partir de tisserand pour fody est disponible.)
un outil très pratique que j'ai utilisé pour mes projets est Notify Property Weaver Fody .
il s'installe comme une étape de construction dans vos projets et lors de la compilation injecte du code qui élève l'événement PropertyChanged
.
faire des propriétés soulever PropertyChanged est fait en mettant attributs spéciaux sur eux:
[ImplementPropertyChanged]
public string MyProperty { get; set; }
en bonus, vous pouvez également spécifier des relations pour les propriétés qui dépendent d'autres propriétés
[ImplementPropertyChanged]
public double Radius { get; set; }
[DependsOn("Radius")]
public double Area
{
get { return Radius * Radius * Math.PI; }
}
le nom de l'opérateur a été implémenté en C# 6.0 avec .NET 4.6 et VS2015 en juillet 2015. Ce qui suit est toujours valable pour C# < 6.0
nous utilisons le code ci-dessous (de http://www.ingebrigtsen.info/post/2008/12/11/INotifyPropertyChanged-revisited.aspx ). Fonctionne très bien :)
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);
}
};
}
}
utilisé par exemple de cette façon:
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);
il peut y avoir des erreurs de syntaxe dans l'exemple. Ne pas le tester. Mais vous devriez avoir le concept il y a au moins :)
EDIT: je vois maintenant que vous avez voulu encore moins de travail, mais oui... les choses ci-dessus au moins rend beaucoup plus facile. Et vous empêchez tous les problèmes effrayants en se référant aux propriétés en utilisant des chaînes.
le cadre 4.5 nous fournit le CallerMemberNameAttribute
, ce qui rend inutile de passer le nom de la propriété comme chaîne de caractères:
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 ...
}
semblable à la solution de Svish, vient de remplacer LAMBDA awesomeness avec la fonctionnalité de cadre de forage ; -)
si vous travaillez sur Framework 4.0 avec kb2468871 installé, vous pouvez installer le Microsoft BCL compatibilité Pack via nuget , qui fournit également cet attribut.
mettre en Œuvre un type de coffre-fort INotifyPropertyChanged
: Voir ici
alors faites votre propre code snippet:
private $Type$ _$PropertyName$;
public $Type$ $PropertyName$
{
get
{
return _$PropertyName$;
}
set
{
if(value != _$PropertyName$)
{
_$PropertyName$ = value;
OnPropertyChanged(o => o.$PropertyName$);
}
}
}
Avec "151990920 extrait de Code" designer et vous l'avez fait ! Façon facile et sûre d'acheter votre INotifyPropertyChanged.
vous pouvez avoir la méthode d'extension sur votre propre délégué Changéet l'utiliser comme ceci:
public string Name
{
get { return name; }
set
{
name = value;
PropertyChanged.Raise(() => Name);
}
}
souscription à un changement de propriété spécifique:
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;
La méthode d'extension permet de déterminer le nom de l'expéditeur et de la propriété simplement en examinant l'expression lambda tree et sans impact majeur sur les performances :
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;
}
}
si PropertyChanged
événement est déclaré dans un type de base alors il ne sera pas visible en tant que délégué de terrain dans les classes dérivées. Dans ce cas, une solution consiste à déclarer un champ protégé de type PropertyChangedEventHandler
et à implémenter explicitement les accesseurs add
et remove
de l'événement:
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);
}
}
}
Je ne connais pas de méthode standard, mais je connais deux solutions de rechange:
1) PostSharp peut le faire pour vous après la compilation. Il est très utile, mais il prend un certain temps sur chaque construction.
2) Outil personnalisé i Visual Studio. Vous pouvez les combiner avec "classe partielle". Ensuite, vous pouvez créer un outil personnalisé pour votre XML et vous pouvez générer du code source à partir du xml.
par exemple ce xml:
<type scope="public" type="class" name="MyClass">
<property scope="public" type="string" modifier="virtual" name="Text" notify="true" />
</type>
peut être la source de ce code:
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" );
}
}
}
vous pouvez regarder château ou Spring.NET et implémenter la fonctionnalité d'intercepteur?
amélioration pour appeler événement dans les classes d'enfants:
a appelé grâce à: ce.NotifyPropertyChange(() => PageIndex);
ajouter ce qui suit dans la catégorie "extensions de notification":
/// <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) });
}
}
je viens de trouver ActiveSharp - Automatic INotifyPropertyChanged , je dois encore l'utiliser, mais il semble bon.
pour citer son site web...
envoyer des notifications de changement de propriété sans spécifier le nom de la propriété comme un chaîne.
à la place, écrivez des propriétés comme ceci:
public int Foo
{
get { return _foo; }
set { SetValue(ref _foo, value); } // <-- no property name here
}
notez qu'il y a pas besoin d'inclure le nom de la propriété en tant que chaîne. ActiveSharp calcule cela de manière fiable et correcte pour lui-même. Il fonctionne sur la base du fait que votre mise en œuvre de propriété passe le champ de soutien (_foo) par ref. (ActiveSharp utilise cet appel "by ref" pour identifier le champ de soutien qui a été passé, et à partir du champ il identifie la propriété).
Juste pour faire de la mise en œuvre plus rapide vous pouvez utiliser un extrait de
de http://aaron-hoffman.blogspot.it/2010/09/visual-studio-code-snippet-for-notify.html
les classes de projets ViewModel suivant le modèle M-V-VM il est souvent nécessaire de soulever un événement "PropertyChanged" (pour aider à la mise en œuvre de l'interface INotifyPropertyChanged) de l'intérieur d'une propriété, setter. C'est une tâche fastidieuse qui, espérons-le, sera un jour résolue en utilisant le compilateur comme un Service...
snippet core (pour lequel full credit va à l'auteur, qui n'est pas moi) est le suivant
<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>
il n'y a pas une seule implémentation de propriété modifiée qui puisse gérer toutes les façons dont les gens veulent l'utiliser. meilleur pari est de générer une classe d'aide pour faire le travail pour vous voici un exemple de celui que j'utilise
/// <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];
}
}
modifier: il a été suggéré que je passe d'une classe helper à une classe value wrapper et j'ai depuis utilisé celui-ci et je trouve qu'il fonctionne très bien
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;
}
exemple d'utilisation
private NotifyValue<int> _val;
public const string ValueProperty = "Value";
public int Value
{
get { return _val.Value; }
set { _val.Value = value; }
}
puis dans le constructeur vous faites
_val = new NotifyValue<int>(this,0,PropertyChanged,PropertyChanging,ValueProperty );
utilisez simplement ce attribut au-dessus de votre déclaration de propriété automatique
[NotifyParentProperty(true)]
public object YourProperty { get; set; }