web-dev-qa-db-fra.com

MVVM dans WPF - Comment alerter ViewModel des modifications apportées au modèle ... ou devrais-je le faire?

Je suis en train de parcourir quelques articles sur MVVM, principalement this et this .

Ma question spécifique est: Comment puis-je communiquer les modifications de modèle du modèle au ViewModel? 

Dans l'article de Josh, je ne vois pas qu'il le fasse. ViewModel demande toujours au modèle des propriétés. Dans l'exemple de Rachel, elle a le modèle implémenté INotifyPropertyChanged et soulève des événements à partir du modèle, mais ils sont destinés à la consommation par la vue elle-même (voir son article/code pour plus de détails sur la raison pour laquelle elle le fait). 

Je ne vois nulle part d'exemples où le modèle alerte le ViewModel des modifications apportées aux propriétés du modèle. Cela m'a fait craindre que ce ne soit peut-être pas fait pour une raison quelconque. Existe-t-il un modèle pour alerter le ViewModel des modifications apportées au modèle? Il semblerait que cela soit nécessaire dans la mesure où (1) il est envisageable qu'il existe plus d'un ViewModel pour chaque modèle et (2) même s'il n'y a qu'un ViewModel, certaines actions sur le modèle peuvent entraîner la modification d'autres propriétés. 

Je soupçonne qu'il pourrait y avoir des réponses/commentaires de la forme "Pourquoi voudriez-vous faire cela?" commentaires, alors voici une description de mon programme. Je suis nouveau sur MVVM, alors peut-être que toute ma conception est défectueuse. 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 BlackJack. 

J'ai une vue qui ne contient aucun code et qui repose uniquement sur la liaison aux propriétés et aux commandes du ViewModel (voir l'article de Josh Smith). 

Pour le meilleur ou pour le pire, j’ai pensé que le modèle ne devrait pas contenir uniquement des classes telles que PlayingCard, Deck, mais également la classe BlackJackGame qui conserve l’état de la partie et permet de savoir quand le joueur a fait faillite, le donneur doit tirer cartes, et quel est le score actuel du joueur et du donneur (moins de 21, 21, buste, etc.). 

De BlackJackGame j'expose des méthodes telles que "DrawCard" et il m'est apparu que lorsqu'une carte est dessinée, des propriétés telles que CardScore et IsBust doivent être mises à jour et ces nouvelles valeurs communiquées au ViewModel. C'est peut-être une mauvaise idée? 

On pourrait penser que le ViewModel a appelé la méthode DrawCard() afin qu’il sache qu’il doit demander un score mis à jour et déterminer s’il fait faillite ou non. Des avis? 

Dans mon ViewModel, j'ai la logique de saisir une image réelle d'une carte à jouer (en fonction de la couleur, du rang) et de la rendre disponible pour la vue. Le modèle ne devrait pas être concerné par cela (peut-être qu'un autre ViewModel utilisera simplement des nombres au lieu d'images jouées sur des cartes). 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.

96
Dave

Si vous souhaitez que vos modèles avertissent les changements de ViewModels, ils doivent implémenter INotifyPropertyChanged , et les ViewModels doivent s'abonner pour recevoir les notifications de PropertyChange.

Votre code pourrait ressembler à quelque chose comme ça:

// Attach EventHandler
PlayerModel.PropertyChanged += PlayerModel_PropertyChanged;

...

// When property gets changed in the Model, raise the PropertyChanged 
// event of the ViewModel copy of the property
PlayerModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "SomeProperty")
        RaisePropertyChanged("ViewModelCopyOfSomeProperty");
}

Mais cela n’est généralement nécessaire que si plusieurs objets apportent des modifications aux données du modèle, ce qui n’est généralement pas le cas.

Si vous rencontrez un cas dans lequel vous ne disposez pas d'une référence à votre propriété Model pour y associer l'événement PropertyChanged, vous pouvez utiliser un système de messagerie tel que EventAggregator de Prism ou Messenger de MVVM Light.

J'ai un bref aperçu des systèmes de messagerie sur mon blog. Cependant, pour le résumer, tout objet peut diffuser un message et tout objet peut s'abonner pour écouter des messages spécifiques. Vous pouvez donc diffuser un objet PlayerScoreHasChangedMessage et un autre objet peut s'abonner pour écouter ces types de messages et mettre à jour sa propriété PlayerScore lorsqu'il en entend 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 ne sont que les blocs utilisés pour créer votre application. Ils ne contiennent généralement que des données, de sorte qu'ils n'auraient pas de méthodes telles que DrawCard() (ce serait dans un ViewModel)

Donc, vous auriez probablement des objets de données Model simples comme ceux-ci:

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

class PlayerModel 
{
    ObservableCollection<Card> FaceUpCards;
    ObservableCollection<Card> FaceDownCards;
    int CurrentScore;

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

et vous auriez un objet ViewModel comme

public class GameViewModel
{
    ObservableCollection<CardModel> Deck;
    PlayerModel Dealer;
    PlayerModel Player;

    ICommand DrawCardCommand;

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

        if (currentPlayer.IsBust)
            // Process next player turn

        Deck.Remove(nextCard);
    }
}

(Les objets ci-dessus doivent tous implémenter INotifyPropertyChanged, mais je l'ai laissé de côté pour des raisons de simplicité)

55
Rachel

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

Dans votre exemple, les modèles sont mis à jour "par eux-mêmes" et ces modifications doivent bien sûr être propagées aux vues. Étant donné que les vues ne peuvent accéder directement qu'aux modèles de vue, cela signifie que le modèle doit communiquer ces modifications au modèle de vue correspondant. Le mécanisme établi pour le faire est bien sûr INotifyPropertyChanged, ce qui signifie que vous obtiendrez un flux de travail comme celui-ci:

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

D'un autre côté, si vos modèles contenaient peu (ou pas) de logique métier ou si, pour une autre raison (telle que l'obtention d'une capacité transactionnelle), vous décidiez de laisser chaque modèle de vue "posséder" son modèle intégré, toutes les modifications apportées au modèle seraient alors répercutées. le modèle de vue donc un tel arrangement ne serait pas nécessaire.

Je décris une telle conception dans une autre question MVVM ici .

21
Jon

Vos choix: 

  • Implémenter INotifyPropertyChanged
  • Événements
  • POCO avec manipulateur de proxy

À mon avis, INotifyPropertyChanged est un élément fondamental de .Net. c'est-à-dire qu'il est dans System.dll. L'implémenter dans votre "Modèle" s'apparente à l'implémentation d'une structure d'événement. 

Si vous voulez du pur POCO, vous devez alors manipuler vos objets via des proxies/services, puis votre ViewModel est informé des modifications en écoutant le proxy. 

Personnellement, je viens d'implémenter INotifyPropertyChanged, puis d'utiliser FODY pour faire le sale boulot pour moi. Il ressemble et se sent POCO. 

Un exemple (en utilisant FODY pour IL Weave les élévateurs de PropertyChanged): 

public class NearlyPOCO: INotifyPropertyChanged
{
     public string ValueA {get;set;}
     public string ValueB {get;set;}

     public event PropertyChangedEventHandler PropertyChanged;
}

vous pouvez ensuite laisser votre ViewModel écouter PropertyChanged pour tout changement; ou des changements spécifiques à la propriété. 

La beauté de la route INotifyPropertyChanged est de l’enchaîner avec un Extended ObservableCollection . Ainsi, vous déposez vos objets quasi poco dans une collection et écoutez-la ... si quelque chose change, où que vous soyez, vous en apprendrez plus.

Soyons honnêtes, ceci pourrait rejoindre la discussion "Pourquoi INotifyPropertyChanged n'a-t-elle pas été gérée automatiquement par le compilateur", qui est dévolue à: Chaque objet de c # doit avoir la possibilité de notifier si une partie de celle-ci a été modifiée; c'est-à-dire implémenter INotifyPropertyChanged par défaut. Mais ce n’est pas le cas et le meilleur itinéraire, qui nécessite le moins d’effort, consiste à utiliser IL Weaving (en particulier _FODY ). 

3
Meirion Hughes

Assez vieux fil, mais après de nombreuses recherches, j'ai proposé ma propre solution: Un PropertyChangedProxy

Avec cette classe, vous pouvez facilement vous inscrire à NotifyPropertyChanged d'une autre personne et prendre les mesures appropriées si elle est déclenchée pour la propriété enregistrée.

Voici un exemple de ce à quoi pourrait ressembler une propriété de modèle "Status" qui peut changer par elle-même et doit ensuite automatiquement avertir le ViewModel de déclencher sa propre propriété PropertyChanged sur sa propriété "Status" afin que la vue soit également notifiée: )

public class MyModel : INotifyPropertyChanged
{
    private string _status;
    public string Status
    {
        get { return _status; }
        set { _status = value; OnPropertyChanged(); }
    }

    // Default INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class MyViewModel : INotifyPropertyChanged
{
    public string Status
    {
        get { return _model.Status; }
    }

    private PropertyChangedProxy<MyModel, string> _statusPropertyChangedProxy;
    private MyModel _model;
    public MyViewModel(MyModel model)
    {
        _model = model;
        _statusPropertyChangedProxy = new PropertyChangedProxy<MyModel, string>(
            _model, myModel => myModel.Status, s => OnPropertyChanged("Status")
        );
    }

    // Default INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

et voici la classe elle-même:

/// <summary>
/// Proxy class to easily take actions when a specific property in the "source" changed
/// </summary>
/// Last updated: 20.01.2015
/// <typeparam name="TSource">Type of the source</typeparam>
/// <typeparam name="TPropType">Type of the property</typeparam>
public class PropertyChangedProxy<TSource, TPropType> where TSource : INotifyPropertyChanged
{
    private readonly Func<TSource, TPropType> _getValueFunc;
    private readonly TSource _source;
    private readonly Action<TPropType> _onPropertyChanged;
    private readonly string _modelPropertyname;

    /// <summary>
    /// Constructor for a property changed proxy
    /// </summary>
    /// <param name="source">The source object to listen for property changes</param>
    /// <param name="selectorExpression">Expression to the property of the source</param>
    /// <param name="onPropertyChanged">Action to take when a property changed was fired</param>
    public PropertyChangedProxy(TSource source, Expression<Func<TSource, TPropType>> selectorExpression, Action<TPropType> onPropertyChanged)
    {
        _source = source;
        _onPropertyChanged = onPropertyChanged;
        // Property "getter" to get the value
        _getValueFunc = selectorExpression.Compile();
        // Name of the property
        var body = (MemberExpression)selectorExpression.Body;
        _modelPropertyname = body.Member.Name;
        // Changed event
        _source.PropertyChanged += SourcePropertyChanged;
    }

    private void SourcePropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == _modelPropertyname)
        {
            _onPropertyChanged(_getValueFunc(_source));
        }
    }
}
3
Roemer

J'ai trouvé cet article utile: http://social.msdn.Microsoft.com/Forums/vstudio/en-US/3eb70678-c216-414f-a4a5-e1e3e557bb95/mvvm-businesslogic-is-part-of -the-? forum = wpf

Mon résumé:

L’idée de l’organisation MVVM est de permettre une réutilisation plus facile des vues et des modèles, ainsi que des tests découplés. Votre modèle de vue est un modèle représentant les entités de vue, votre modèle représente les entités commerciales.

Et si vous vouliez faire un jeu de poker plus tard? Une grande partie de l'interface utilisateur devrait être réutilisable. Si votre logique de jeu est liée à votre modèle de vue, il serait très difficile de réutiliser ces éléments sans avoir à reprogrammer le modèle de vue. Et si vous voulez changer votre interface utilisateur? Si votre logique de jeu est couplée à votre logique de modèle de vue, vous devrez vérifier à nouveau que votre jeu fonctionne toujours. Que faire si vous voulez créer un bureau et une application Web? Si votre modèle de vue contient la logique du jeu, il deviendrait compliqué d'essayer de conserver ces deux applications côte à côte, car la logique de l'application serait inévitablement liée à la logique métier du modèle de vue.

Les notifications de modification de données et la validation des données se produisent dans chaque couche (la vue, le modèle de vue et le modèle).

Le modèle contient vos représentations de données (entités) et votre logique applicative spécifiques à ces entités. Un jeu de cartes est une "chose" logique avec des propriétés inhérentes. Un bon deck ne peut contenir des cartes en double. Il doit exposer un moyen d’obtenir la ou les cartes du dessus. Il faut savoir ne pas donner plus de cartes qu'il n'en reste. De tels comportements font partie du modèle car ils sont inhérents à un jeu de cartes. Il y aura également des modèles de revendeur, des modèles de joueur, des modèles de main, etc. Ces modèles peuvent et vont interagir.

Le modèle de vue comprendrait la présentation et la logique d'application. Tout le travail associé à l'affichage du jeu est séparé de la logique du jeu. Cela pourrait inclure l’affichage des mains sous forme d’images, les demandes de cartes adressées au modèle du concessionnaire, les paramètres d’affichage de l’utilisateur, etc.

Les tripes de l'article:

En gros, la façon dont je me plais à expliquer cela, c’est votre affaire la logique et les entités constituent le modèle. C'est ce que votre spécifique l'application utilise, mais pourrait être partagée entre de nombreuses applications.

La vue est la couche de présentation - tout ce qui concerne réellement interfacer directement avec l'utilisateur.

ViewModel est fondamentalement la "colle" spécifique à votre application qui relie les deux ensemble.

J'ai un joli diagramme ici qui montre comment ils s'interfacent: 

http://reedcopsey.com/2010/01/06/better-user-and-developer-experiences-from-windows-forms-to-wpf-with-mvvm-part-7-mvvm/

Dans votre cas, abordons certaines des spécificités ...

Validation: Cela se présente généralement sous 2 formes. La validation liée à l'entrée utilisateur se produirait dans le ViewModel (principalement) et la vue (c.-à-d.: une zone de texte "numérique" empêchant la saisie de texte est gérée pour vous dans la vue, etc.). En tant que tel, la validation de l'entrée de l'utilisateur est généralement un problème VM. Cela étant dit, il y a souvent un deuxième "couche" de validation - il s’agit de la validation des données être utilisé correspond aux règles commerciales. Cela fait souvent partie du model lui-même - lorsque vous transmettez des données à votre modèle, cela peut provoquer erreurs de validation. La VM devra alors remapper ces informations Retour à la vue.

Opérations "dans les coulisses sans vue, comme écrire dans DB, Envoyer un courrier électronique, etc.": cela fait vraiment partie des "Opérations spécifiques à un domaine" dans mon diagramme et fait vraiment partie du modèle. C'est ce que vous essayez d'exposer via l'application. Le ViewModel agit comme un pont pour exposer ces informations, mais le les opérations sont pur-Model.

Opérations pour le ViewModel: Le ViewModel a besoin de plus que du simple INPC - elle nécessite également toute opération spécifique à votre application (et non à votre logique métier), telle que la sauvegarde des préférences et l'état de l'utilisateur, etc. Cela va varier app. par application, même lors de l'interfaçage du même "modèle". 

Une bonne façon de penser - Disons que vous voulez faire 2 versions de votre système de commande. Le premier est en WPF et le second est un site Web interface.

La logique partagée qui traite les commandes elles-mêmes (envoi d’e-mails , Saisie dans une base de données, etc.) constitue le modèle. Votre application est exposer ces opérations et données à l'utilisateur, mais en 2 façons.

Dans l'application WPF, l'interface utilisateur (avec laquelle le spectateur interagit Avec) est la "vue" - dans l'application Web, il s'agit essentiellement de la code qui (au moins éventuellement) est transformé en javascript + html + css sur le client.

Le ViewModel est le reste de la "colle" nécessaire pour adapter votre modèle (ces opérations liées à la commande) afin de le faire fonctionner avec la technologie/couche de vue spécifique que vous utilisez.

2
VoteCoffee

La notification basée sur INotifyPropertyChanged et INotifyCollectionChanged est exactement ce dont vous avez besoin. Pour vous simplifier la vie avec les modifications apportées aux propriétés, la validation au moment de la compilation du nom de la propriété, afin d’éviter les fuites de mémoire, je vous conseillerais d’utiliser PropertyObserver à partir de MVVM Foundation de Josh Smith . Comme ce projet est open source, vous pouvez ajouter uniquement cette classe à votre projet à partir de sources.

Pour comprendre, comment utiliser PropertyObserver lire cet article .

Aussi, jetez un œil plus profond sur Les extensions réactives (Rx) . Vous pouvez exposer IObserver <T> à partir de votre modèle et vous y abonner dans le modèle d'affichage.

2
Vladimir Dorokhov

Les gars ont fait un travail incroyable en répondant à cette question, mais dans des situations comme celle-ci, j’ai vraiment le sentiment que le modèle MVVM est pénible, c’est pourquoi je voudrais utiliser un contrôleur supervisant ou une approche de vue passive et abandonner au moins le système de reliure pour les objets modèles qui sont générer des changements sur leurs propres.

1
Ibrahim Najjar

Je préconise depuis longtemps le modèle directionnel -> Afficher le modèle -> Afficher le flux de modifications, comme vous pouvez le constater dans la section Flux de modifications de mon article MVVM à partir de 2008. Cela nécessite implémenter INotifyPropertyChanged sur le modèle. Autant que je sache, c'est depuis devenu une pratique courante.

Parce que vous avez mentionné Josh Smith, jetez un œil à sa classe PropertyChanged . C'est une classe d'assistance pour s'abonner à l'événement INotifyPropertyChanged.PropertyChanged du modèle.

Vous pouvez réellement aller plus loin dans cette approche, comme je l’ai récemment fait en créant ma classe PropertiesUpdater . Les propriétés du modèle de vue sont calculées sous forme d'expressions complexes comprenant une ou plusieurs propriétés du modèle.

1
HappyNomad

Il n'y a rien de mal à implémenter INotifyPropertyChanged dans Model et à l'écouter dans ViewModel. En fait, vous pouvez même accéder à la propriété du modèle en XAML: {Binding Model.ModelProperty}

En ce qui concerne les propriétés en lecture seule dépendantes/calculées, je n’ai encore rien vu de mieux et aussi simplement que ceci: https://github.com/StephenCleary/CalculatedProperties . C'est très simple mais incroyablement utile, il s'agit en réalité de "formules Excel pour MVVM" - fonctionne de la même manière que Excel, en propageant les modifications apportées aux cellules de formule sans effort supplémentaire de votre part.

0
KolA

Vous pouvez générer des événements à partir du modèle, auxquels le modèle de vue doit souscrire.

Par exemple, j'ai récemment travaillé sur un projet pour lequel je devais générer un arbre (naturellement, le modèle avait une nature hiérarchique). Dans le modèle, j'avais une collection observable appelée ChildElements.

Dans le modèle de vue, j'avais stocké une référence à l'objet dans le modèle et avais souscrit à l'événement CollectionChanged de la collection observable, comme suit: ModelObject.ChildElements.CollectionChanged += new CollectionChangedEventHandler(insert function reference here)...

Ensuite, votre modèle de vue reçoit automatiquement une notification lorsqu'un changement intervient dans le modèle. Vous pouvez suivre le même concept en utilisant PropertyChanged, mais vous devrez explicitement générer des événements de modification de propriété à partir de votre modèle pour que cela fonctionne.

0
Mash

Cela me semble être une question très importante - même quand il n'y a pas de pression pour le faire. Je travaille sur un projet de test, qui implique une TreeView. Il existe des éléments de menu et des éléments mappés à des commandes, par exemple Supprimer. Actuellement, je mets à jour le modèle et le modèle de vue à partir de celui-ci.

Par exemple,

public void DeleteItemExecute ()
{
    DesignObjectViewModel node = this.SelectedNode;    // Action is on selected item
    DocStructureManagement.DeleteNode(node.DesignObject); // Remove from application
    node.Remove();                                // Remove from view model
    Controller.UpdateDocument();                  // Signal document has changed
}

Ceci est simple, mais semble avoir un défaut très fondamental. Un test unitaire typique exécuterait la commande, puis vérifierait le résultat dans le modèle de vue. Mais cela ne teste pas que la mise à jour du modèle était correcte, car les deux sont mis à jour simultanément.

Il est donc peut-être préférable d'utiliser des techniques telles que PropertyObserver pour laisser la mise à jour du modèle déclencher une mise à jour du modèle de vue. Le même test unitaire ne fonctionnerait désormais que si les deux actions aboutissaient.

Je me rends compte que ce n’est pas une réponse potentielle, mais cela semble valoir la peine d’être publié.

0
Art