Je recherche une solution propre et élégante pour gérer l'événement INotifyPropertyChanged
d'objets imbriqués (enfants). Exemple de code:
public class Person : INotifyPropertyChanged {
private string _firstName;
private int _age;
private Person _bestFriend;
public string FirstName {
get { return _firstName; }
set {
// Short implementation for simplicity reasons
_firstName = value;
RaisePropertyChanged("FirstName");
}
}
public int Age {
get { return _age; }
set {
// Short implementation for simplicity reasons
_age = value;
RaisePropertyChanged("Age");
}
}
public Person BestFriend {
get { return _bestFriend; }
set {
// - Unsubscribe from _bestFriend's INotifyPropertyChanged Event
// if not null
_bestFriend = value;
RaisePropertyChanged("BestFriend");
// - Subscribe to _bestFriend's INotifyPropertyChanged Event if not null
// - When _bestFriend's INotifyPropertyChanged Event is fired, i'd like
// to have the RaisePropertyChanged("BestFriend") method invoked
// - Also, I guess some kind of *weak* event handler is required
// if a Person instance i beeing destroyed
}
}
// **INotifyPropertyChanged implementation**
// Implementation of RaisePropertyChanged method
}
Concentrez-vous sur la propriété BestFriend
et sa valeur. Maintenant Je sais que je pourrais le faire manuellement , en appliquant toutes les étapes décrites dans les commentaires. Mais cela va faire beaucoup de code, surtout quand je prévois de nombreuses propriétés enfants implémentant INotifyPropertyChanged
comme ceci. Bien sûr, ils ne seront pas toujours du même type, la seule chose qu'ils ont en commun est l'interface INotifyPropertyChanged
.
La raison en est que, dans mon scénario réel, j’ai un objet "Item" (dans le panier) complexe qui a imbriqué les propriétés de l’objet sur plusieurs calques (Item ayant un objet "License", qui peut lui-même avoir à nouveau des objets enfants) et je besoin d'être averti de tout changement unique de "l'article" pour pouvoir recalculer le prix.
Avez-vous de bons conseils ou même une mise en œuvre Pour m'aider à résoudre Cela?
Malheureusement, je ne suis pas capable/autorisé à utiliser des étapes post-build comme PostSharp pour atteindre mon objectif.
Merci beaucoup d'avance,
- Thomas
comme je ne pouvais pas trouver une solution prête à l'emploi, j'ai réalisé une implémentation personnalisée basée sur les suggestions de Pieters (et Marks) (merci!).
En utilisant les classes, vous serez averti de tout changement dans une arborescence d'objets profonde, cela fonctionne pour toute INotifyPropertyChanged
implémentant des types et INotifyCollectionChanged
* mettant en œuvre des collections (Évidemment, j'utilise la ObservableCollection
pour cela).
J'espère que cette solution s'est révélée assez épurée et élégante. Toutefois, elle n'a pas encore été testée et des améliorations sont possibles. C'est assez facile à utiliser, il suffit de créer une instance de ChangeListener
en utilisant sa méthode statique Create
et en passant votre INotifyPropertyChanged
:
var listener = ChangeListener.Create(myViewModel);
listener.PropertyChanged +=
new PropertyChangedEventHandler(listener_PropertyChanged);
les PropertyChangedEventArgs
fournissent une PropertyName
qui sera toujours le "chemin" complet de vos objets. Par exemple, si vous modifiez le nom "Meilleur ami" de votre personne, la PropertyName
sera "BestFriend.Name", si la BestFriend
contient une collection d'enfants et que vous changez son âge, la valeur sera "BestFriend.Children []. Age" etc. N'oubliez pas de Dispose
lorsque votre objet est détruit, il sera (espérons-le) totalement désabonné de tous les écouteurs d'événement.
Il compile en .NET (testé dans 4) et Silverlight (testé dans 4). Parce que le code est séparé en trois classes, j'ai posté le code sur Gist 705450 où vous pouvez tout récupérer: https://Gist.github.com/705450 **
*) Une des raisons pour lesquelles le code fonctionne est que ObservableCollection
implémente également INotifyPropertyChanged
; sinon, il ne fonctionnerait pas comme vous le souhaitez, il s'agit d'un avertissement connu.
**) Utilisation gratuite, publiée sous MIT Licence
Je pense que vous recherchez quelque chose comme une reliure WPF.
Le fonctionnement de INotifyPropertyChanged
est que la RaisePropertyChanged("BestFriend");
doit uniquement être remplie lorsque la propriété BestFriend
change. Pas quand quelque chose sur l'objet lui-même change.
Vous pouvez implémenter cela avec un gestionnaire d'événements INotifyPropertyChanged
en deux étapes. Votre auditeur s’enregistrerait sur l’événement modifié de la Person
. Lorsque la variable BestFriend
est définie/modifiée, vous vous enregistrez sur l'événement modifié de la variable BestFriend
Person
. Ensuite, vous commencez à écouter les événements modifiés de cet objet.
C’est exactement comment la liaison WPF implémente cela. L'écoute des modifications des objets imbriqués se fait via ce système.
La raison pour laquelle cela ne fonctionnera pas lorsque vous l'implémenterez dans Person
est que les niveaux peuvent devenir très profonds et que l'événement modifié de BestFriend
ne signifie plus rien ("qu'est-ce qui a changé?") Ce problème devient plus important lorsque vous avez des relations circulaires où, par exemple, Le meilleur ami de votre monteur est la mère de votre meilleur démon. Ensuite, lorsque l'une des propriétés change, vous obtenez un débordement de pile.
La solution à ce problème consiste donc à créer une classe avec laquelle vous pouvez créer des auditeurs. Vous pouvez par exemple construire un écouteur sur BestFriend.FirstName
. Cette classe mettrait alors un gestionnaire d’événements sur l’événement modifié de Person
et écouterait les modifications sur BestFriend
. Ensuite, lorsque cela change, il place un auditeur sur BestFriend
et écoute les modifications de FirstName
. Ensuite, lorsque cela change, il envoie un événement et vous pouvez l'écouter. Voilà en gros comment fonctionne la reliure WPF.
Voir http://msdn.Microsoft.com/en-us/library/ms750413.aspx pour plus d'informations sur la liaison WPF.
Solution intéressante Thomas.
J'ai trouvé une autre solution. C'est ce qu'on appelle le modèle de conception du propagateur. Vous pouvez trouver plus d'informations sur le Web (par exemple, sur CodeProject: Le propagateur en C # - Alternative au modèle de conception Observer ).
Fondamentalement, c'est un modèle pour mettre à jour des objets dans un réseau de dépendance. C'est très utile lorsque les changements d'état doivent être poussés à travers un réseau d'objets. Un changement d'état est représenté par un objet lui-même qui traverse le réseau de propagateurs. En encapsulant le changement d'état en tant qu'objet, les propagateurs deviennent faiblement couplés.
Diagramme de classes des classes du propagateur réutilisables:
En savoir plus sur CodeProject .
Découvrez ma solution sur CodeProject: http://www.codeproject.com/Articles/775831/INotifyPropertyChanged-propagator Il fait exactement ce dont vous avez besoin - aide à la propagation (de manière élégante) propriétés dépendantes lorsque des dépendances pertinentes dans ce modèle de vue ou dans tout modèle de vue imbriquée changent:
public decimal ExchTotalPrice
{
get
{
RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice));
RaiseMeWhen(ExchangeRate, has => has.Changed(_ => _.Rate));
return TotalPrice * ExchangeRate.Rate;
}
}
J'ai écrit un assistant facile à faire. Vous appelez simplement BubblePropertyChanged (x => x.BestFriend) dans votre modèle de vue parent. nb il existe une hypothèse que vous avez une méthode appelée NotifyPropertyChagned dans votre parent, mais vous pouvez l'adapter.
/// <summary>
/// Bubbles up property changed events from a child viewmodel that implements {INotifyPropertyChanged} to the parent keeping
/// the naming hierarchy in place.
/// This is useful for nested view models.
/// </summary>
/// <param name="property">Child property that is a viewmodel implementing INotifyPropertyChanged.</param>
/// <returns></returns>
public IDisposable BubblePropertyChanged(Expression<Func<INotifyPropertyChanged>> property)
{
// This step is relatively expensive but only called once during setup.
MemberExpression body = (MemberExpression)property.Body;
var prefix = body.Member.Name + ".";
INotifyPropertyChanged child = property.Compile().Invoke();
PropertyChangedEventHandler handler = (sender, e) =>
{
this.NotifyPropertyChanged(prefix + e.PropertyName);
};
child.PropertyChanged += handler;
return Disposable.Create(() => { child.PropertyChanged -= handler; });
}
Cela fait un jour que je fais des recherches sur le Web et j'ai trouvé une autre solution intéressante de Sacha Barber:
http://www.codeproject.com/Articles/166530/A-Chained-Property-Observer
Il a créé des références faibles au sein d'un observateur de propriétés chaîné. Consultez l'article si vous voulez voir un autre excellent moyen d'implémenter cette fonctionnalité.
Et je tiens également à mentionner une implémentation de Nice avec les extensions réactives @ http://www.rowanbeach.com/rowan-beach-blog/a-system-reactive-property-change-observer/
Cette solution fonctionne uniquement pour un niveau d'observateur, pas pour une chaîne complète d'observateurs.