131 votes

MVVM in WPF - Comment avertir le ViewModel des changements dans le modèle... ou devrais-je le faire?

Je suis en train de parcourir quelques articles sur le MVVM, principalement celui-ci et celui-ci.

Ma question spécifique est : Comment communiquer les changements du modèle du Model au ViewModel?

Dans l'article de Josh, je ne vois pas qu'il fasse cela. Le ViewModel demande toujours les propriétés du modèle. Dans l'exemple de Rachel, elle demande que le modèle implémente INotifyPropertyChanged, et déclenche des événements du modèle, mais ceux-ci sont destinés à être consommés par la vue elle-même (voir son article/code pour plus de détails sur pourquoi elle fait cela).

Nulle part je ne vois d'exemples où le modèle alerte le ViewModel des changements aux propriétés du modèle. Cela me préoccupe peut-être que ce ne soit pas fait pour une raison. Existe-t-il une norme pour informer le ViewModel des changements dans le modèle? Il semblerait que cela soit nécessaire car (1) il pourrait y avoir plus d'un ViewModel pour chaque modèle, et (2) même s'il n'y a qu'un seul ViewModel, une action sur le modèle pourrait entraîner d'autres changements de propriétés.

Je soupçonne qu'il pourrait y avoir des réponses/commentaires du type "Pourquoi voudriez-vous faire cela?" donc voici une description de mon programme. Je suis nouveau dans le MVVM donc peut-être que tout mon design est défectueux. Je vais le décrire brièvement.

Je programme quelque chose de plus intéressant (du moins, pour moi!) que les classes "Client" ou "Produit". Je programme le Blackjack.

J'ai une vue qui n'a pas de code derrière et qui se contente de lier des propriétés et des commandes dans le ViewModel (voir l'article de Josh Smith).

Que cela plaise ou non, j'ai eu l'attitude que le modèle devrait contenir non seulement des classes telles que CarteJouée, Tas, mais aussi la classe JeuBlackJack qui garde l'état de l'ensemble du jeu, et sait quand le joueur a dépassé, que le croupier doit piocher des cartes, et quel est le score actuel du joueur et du croupier (moins de 21, 21, dépassé, etc.).

Depuis JeuBlackJack j'expose des méthodes comme "TirerCarte" et il m'est venu à l'esprit que lorsque une carte est tirée, des propriétés comme ScoreCarte, et EstPété devraient être mises à jour et ces nouvelles valeurs communiquées au ViewModel. Peut-être que c'est une pensée fautive?

On pourrait adopter l'attitude que le ViewModel a appelé la méthode TirerCarte() donc il devrait savoir demander un score actualisé et savoir s'il a dépassé ou non. Des opinions?

Dans mon ViewModel, j'ai la logique pour obtenir une image réelle d'une carte de jeu (basée sur la couleur, le rang) et la rendre disponible pour la vue. Le modèle ne devrait pas se soucier de cela (peut-être qu'un autre ViewModel utiliserait simplement des nombres au lieu d'images de cartes de jeu). Bien sûr, certains me diront peut-être que le modèle ne devrait même pas avoir le concept d'un jeu de Blackjack et que cela devrait être géré dans le ViewModel?

3 votes

L'interaction que vous décrivez semble nécessiter uniquement un mécanisme d'événement standard. Le modèle peut exposer un événement appelé OnBust, et le MV peut s'y abonner. Je suppose que vous pourriez également utiliser une approche IEA.

0 votes

Je vais être honnête, si je devais créer une véritable « application » de blackjack, mes données seraient cachées derrière quelques couches de services/proxies et un niveau pédant de tests unitaires semblables à A+B = C. Ce serait le proxy/service qui informerait des changements.

1 votes

Merci à tous! Malheureusement, je ne peux choisir qu'une seule réponse. Je choisis celle de Rachel en raison des conseils supplémentaires en architecture et du nettoyage de la question d'origine. Mais il y avait beaucoup de bonnes réponses et je les apprécie. -Dave

74voto

Rachel Points 49408

Si vous souhaitez que vos modèles alertent les ViewModels des modifications, ils doivent implémenter INotifyPropertyChanged, et les ViewModels doivent s'abonner pour recevoir des notifications de changement de propriété.

Votre code pourrait ressembler à ceci :

// Attacher EventHandler
PlayerModel.PropertyChanged += PlayerModel_PropertyChanged;

...

// Lorsque la propriété est modifiée dans le modèle, déclencher l'événement PropertyChanged 
// de la copie du ViewModel de la propriété
PlayerModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "SomeProperty")
        RaisePropertyChanged("ViewModelCopyOfSomeProperty");
}

Mais cela est généralement nécessaire uniquement si plus d'un objet modifie les données du modèle, ce qui n'est généralement pas le cas.

Si vous vous trouvez dans une situation où vous n'avez pas réellement une référence à votre propriété du modèle pour attacher l'événement PropertyChanged, vous pouvez utiliser un système de messagerie tel que l'EventAggregator de Prism ou le Messenger de MVVM Light.

J'ai un bref aperçu des systèmes de messagerie sur mon blog, cependant pour le résumer, n'importe quel objet peut diffuser un message, et n'importe quel objet peut s'abonner pour écouter des messages spécifiques. Ainsi, vous pourriez diffuser un PlayerScoreHasChangedMessage d'un objet, et un autre objet pourrait s'abonner pour écouter ces types de messages et mettre à jour sa propriété PlayerScore lorsqu'il en reçoit un.

Mais je ne pense pas que cela soit nécessaire pour le système que vous avez décrit.

Dans un monde MVVM idéal, votre application est composée de vos ViewModels, et vos Modèles sont simplement les blocs utilisés pour construire votre application. Ils contiennent généralement uniquement des données, donc ils n'auraient pas de méthodes telles que DrawCard() (ce serait dans un ViewModel)

Vous auriez donc probablement des objets de données de modèle simples comme ceux-ci :

class CardModel
{
    int Score;
    SuitEnum Suit;
    CardEnum CardValue;
}

class PlayerModel 
{
    ObservableCollection FaceUpCards;
    ObservableCollection FaceDownCards;
    int CurrentScore;

    bool IsBust
    {
        get
        {
            return Score > 21;
        }
    }
}

et vous auriez un objet ViewModel comme ceci

public class GameViewModel
{
    ObservableCollection Deck;
    PlayerModel Dealer;
    PlayerModel Player;

    ICommand DrawCardCommand;

    void DrawCard(Player currentPlayer)
    {
        var nextCard = Deck.First();
        currentPlayer.FaceUpCards.Add(nextCard);

        if (currentPlayer.IsBust)
            // Traiter le prochain tour du joueur

        Deck.Remove(nextCard);
    }
}

(Les objets ci-dessus doivent tous implémenter INotifyPropertyChanged, mais je l'ai omis pour simplifier)

0 votes

Rachel, merci pour la réponse. Tu soulèves un autre bon point (involontairement) - je dois faire plus attention au choix des noms de mes méthodes. Par "DrawCard", je voulais dire "prendre une carte du paquet", pas "dessiner une image d'une carte" ou quoi que ce soit du genre. Est-ce que cela devrait toujours être dans le ViewModel ?

3 votes

Plus généralement, est-ce que toute la logique métier / les règles vont dans le modèle? Où va toute la logique qui dit que l'on peut prendre une carte jusqu'à 21 (mais que le croupier reste à 17), que l'on peut diviser les cartes, etc. J'ai supposé que tout cela appartenait à la classe modèle et c'est pour cette raison que j'ai senti que j'avais besoin d'une classe contrôleur BlacJackGame dans le modèle. J'essaie encore de comprendre cela et j'apprécierai des exemples/références. L'idée du blackjack par exemple a été empruntée à une classe iTunes sur la programmation iOS où la logique métier / les règles se trouvent très certainement dans la classe modèle d'un modèle MVC.

3 votes

@Dave Oui, la méthode DrawCard() serait dans le ViewModel, ainsi que toute votre autre logique de jeu. Dans une application MVVM idéale, vous devriez être en mesure d'exécuter votre application sans l'interface utilisateur entièrement, simplement en créant des ViewModels et en exécutant leurs méthodes, comme à travers un script de test ou une fenêtre d'invite de commande. Les Modèles sont généralement seulement des modèles de données contenant des données brutes et une validation de données de base.

28voto

Jon Points 194296

Réponse courte : tout dépend des détails.

Dans votre exemple, les modèles sont mis à jour "par eux-mêmes" et ces changements doivent bien sûr se propager d'une manière ou d'une autre aux vues. Puisque les vues ne peuvent accéder directement qu'aux viewmodels, cela signifie que le modèle doit communiquer ces changements au viewmodel correspondant. Le mécanisme établi pour le faire est bien sûr INotifyPropertyChanged, ce qui signifie que vous obtiendrez un flux de travail comme suit :

  1. Le viewmodel est créé et encapsule le modèle
  2. Le viewmodel s'abonne à l'événement PropertyChanged du modèle
  3. Le viewmodel est défini comme DataContext de la vue, les propriétés sont liées, etc.
  4. La vue déclenche une action sur le viewmodel
  5. Le viewmodel appelle une méthode sur le modèle
  6. Le modèle se met à jour
  7. Le viewmodel gère le PropertyChanged du modèle et déclenche son propre PropertyChanged en réponse
  8. La vue reflète les changements dans ses liaisons, fermant la boucle de rétroaction

D'autre part, si vos modèles contiennent peu (ou pas du tout) de logique métier, ou si, pour une autre raison (comme obtenir une capacité transactionnelle), vous avez décidé de laisser chaque viewmodel "posséder" son modèle encapsulé, alors toutes les modifications apportées au modèle passeront par le viewmodel et un tel arrangement ne serait pas nécessaire.

Je décris une telle conception dans une autre question sur le modèle MVVM ici.

0 votes

Bonjour, la liste que vous avez faite est brillante. J'ai cependant un problème avec 7. et 8. En particulier : j'ai un ViewModel qui n'implémente pas INotifyPropertyChanged. Il contient une liste d'enfants, qui contient à son tour une liste d'enfants (il est utilisé comme un ViewModel pour un contrôle Treeview WPF). Comment faire en sorte que le DataContext du UserControl ViewModel "écoute" les modifications de propriété de l'un des enfants (TreeviewItems) ? Comment m'abonner exactement à tous les éléments enfants qui implémentent INotifyPropertyChanged ? Ou devrais-je poser une question séparée ?

2voto

Vladimir Dorokhov Points 2581

La notification basée sur INotifyPropertyChanged et INotifyCollectionChanged est exactement ce dont vous avez besoin. Pour simplifier votre vie avec l'abonnement aux modifications de propriété, la validation des noms de propriété au moment de la compilation, en évitant les fuites de mémoire, je vous conseille d'utiliser PropertyObserver de Josh Smith's MVVM Foundation. Comme ce projet est open source, vous pouvez simplement ajouter cette classe à votre projet à partir des sources.

Pour comprendre comment utiliser PropertyObserver, lisez cet article.

De plus, jetez un œil plus en profondeur aux Reactive Extensions (Rx). Vous pouvez exposer IObserver depuis votre modèle et vous y abonner dans le view model.

0 votes

Merci beaucoup de faire référence à l'excellent article de Josh Smith et de couvrir les Weak Events!

2voto

HappyNomad Points 1823

J'ai défendu pendant longtemps le flux de changement directionnel Modèle -> Modèle de vue -> Vue, comme vous pouvez le voir dans la section Évolution des changements de mon article MVVM de 2008. Cela nécessite de mettre en œuvre INotifyPropertyChanged sur le modèle. Autant que je sache, c'est devenu depuis une pratique courante.

Puisque tu as mentionné Josh Smith, jetez un œil à sa classe PropertyChanged. C'est une classe d'aide pour s'abonner à l'événement INotifyPropertyChanged.PropertyChanged du modèle.

En fait, vous pouvez pousser cette approche beaucoup plus loin, comme je l'ai récemment fait en créant ma classe PropertiesUpdater. Les propriétés du vue-modèle sont calculées comme des expressions complexes qui incluent une ou plusieurs propriétés sur le modèle.

2voto

Mash Points 701

Vous pouvez déclencher des événements à partir du modèle, auxquels le viewmodel devrait s'abonner.

Par exemple, j'ai récemment travaillé sur un projet pour lequel j'ai dû générer une vue en arbre (naturellement, le modèle avait une structure hiérarchique). Dans le modèle, j'avais une observablecollection appelée ChildElements.

Dans le viewmodel, j'avais stocké une référence à l'objet dans le modèle, et m'étais abonné à l'événement CollectionChanged de l'observablecollection, comme ceci : ModelObject.ChildElements.CollectionChanged += new CollectionChangedEventHandler(inserer la référence de la fonction ici)...

Ensuite, votre viewmodel est automatiquement notifié lorsqu'un changement se produit dans le modèle. Vous pouvez suivre le même concept en utilisant PropertyChanged, mais vous devrez déclencher explicitement des événements de changement de propriété à partir de votre modèle pour que cela fonctionne.

0 votes

Si vous traitez des données hiérarchiques, vous voudrez regarder Demo 2 de mon article MVVM.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X