web-dev-qa-db-fra.com

S'abonner à INotifyPropertyChanged pour les objets imbriqués (enfants)

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

27
thmshd

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

21
thmshd

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 BestFriendPerson. 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.

16
Pieter van Ginkel

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:

A class diagram of the re-usable Propagator classes

En savoir plus sur CodeProject .

3
Marek Takac

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;
    }
}
0
KolA

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; });
    }
0
DanH

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. 

0
Pascalsz