Model-View-Presenter dans WinForms

90

J'essaye d'implémenter la méthode MVP pour la première fois, en utilisant WinForms.

J'essaie de comprendre la fonction de chaque couche.

Dans mon programme, j'ai un bouton GUI qui, lorsqu'on clique dessus, ouvre une fenêtre openfiledialog.

Ainsi, en utilisant MVP, l'interface graphique gère l'événement de clic de bouton et appelle ensuite presenter.openfile ();

Dans presenter.openfile (), cela devrait-il alors déléguer l'ouverture de ce fichier à la couche de modèle, ou comme il n'y a pas de données ou de logique à traiter, devrait-il simplement agir sur la demande et ouvrir la fenêtre openfiledialog?

Mise à jour: J'ai décidé d'offrir une prime car je sens que j'ai besoin d'une aide supplémentaire à ce sujet, et de préférence adaptée à mes points spécifiques ci-dessous, afin d'avoir le contexte.

D'accord, après avoir lu MVP, j'ai décidé d'implémenter la vue passive. En fait, j'aurai un tas de contrôles sur un Winform qui seront gérés par un présentateur, puis les tâches déléguées au (x) modèle (s). Mes points spécifiques sont ci-dessous:

  1. Lorsque le winform se charge, il doit obtenir une arborescence. Ai-je raison de penser que la vue devrait donc appeler une méthode telle que: presenter.gettree (), celle-ci déléguera à son tour au modèle, qui obtiendra les données pour l'arborescence, la créera et la configurera, la renverra au présentateur, qui à son tour passera à la vue qui l'attribuera alors simplement à, par exemple, un panneau?

  2. Est-ce que ce serait la même chose pour n'importe quel contrôle de données sur le Winform, car j'ai également un datagridview?

  3. Mon application comporte un certain nombre de classes de modèles avec le même assemblage. Il prend également en charge une architecture de plugins avec des plugins qui doivent être chargés au démarrage. La vue appellerait-elle simplement une méthode de présentateur, qui à son tour appellerait une méthode qui charge les plugins et afficherait les informations dans la vue? Quel niveau contrôlerait alors les références du plugin. La vue contiendrait-elle des références à eux ou au présentateur?

  4. Ai-je raison de penser que la vue doit gérer tout ce qui concerne la présentation, de la couleur du nœud de l'arborescence à la taille de la grille de données, etc.?

Je pense que ce sont mes principales préoccupations et si je comprends comment le flux devrait être pour celles-ci, je pense que tout ira bien.

Darren Young
la source
Ce lien lostechies.com/derekgreer/2008/11/23/… explique certains des styles de MVP. Cela pourrait s'avérer utile en plus de l'excellente réponse de Johann.
ak3nat0n

Réponses:

123

C'est mon humble avis sur MVP et vos problèmes spécifiques.

Tout d'abord , tout ce avec lequel un utilisateur peut interagir ou simplement être affiché est une vue . Les lois, le comportement et les caractéristiques d'une telle vue sont décrits par une interface . Cette interface peut être implémentée à l'aide d'une interface utilisateur WinForms, d'une interface utilisateur de console, d'une interface utilisateur Web ou même d'aucune interface utilisateur (généralement lors du test d'un présentateur) - l'implémentation concrète n'a pas d'importance tant qu'elle obéit aux lois de son interface d'affichage .

Deuxièmement , une vue est toujours contrôlée par un présentateur . Les lois, le comportement et les caractéristiques d'un tel présentateur sont également décrits par une interface . Cette interface n'a aucun intérêt dans l'implémentation de la vue concrète tant qu'elle obéit aux lois de son interface de vue.

Troisièmement , comme un présentateur contrôle sa vue, pour minimiser les dépendances, il n'y a vraiment aucun gain à avoir la vue en sachant quoi que ce soit sur son présentateur. Il y a un contrat convenu entre le présentateur et la vue et c'est indiqué par l'interface de vue.

Les implications de Third sont:

  • Le présentateur ne dispose d'aucune méthode que la vue peut appeler, mais la vue contient des événements auxquels le présentateur peut s'abonner.
  • Le présentateur connaît son point de vue. Je préfère accomplir cela avec l'injection du constructeur sur le présentateur concret.
  • La vue n'a aucune idée de quel présentateur la contrôle; il ne sera tout simplement jamais fourni de présentateur.

Pour votre problème, ce qui précède pourrait ressembler à ceci dans un code quelque peu simplifié:

interface IConfigurationView
{
    event EventHandler SelectConfigurationFile;

    void SetConfigurationFile(string fullPath);
    void Show();
}

class ConfigurationView : IConfigurationView
{
    Form form;
    Button selectConfigurationFileButton;
    Label fullPathLabel;

    public event EventHandler SelectConfigurationFile;

    public ConfigurationView()
    {
        // UI initialization.

        this.selectConfigurationFileButton.Click += delegate
        {
            var Handler = this.SelectConfigurationFile;

            if (Handler != null)
            {
                Handler(this, EventArgs.Empty);
            }
        };
    }

    public void SetConfigurationFile(string fullPath)
    {
        this.fullPathLabel.Text = fullPath;
    }

    public void Show()
    {
        this.form.ShowDialog();        
    }
}

interface IConfigurationPresenter
{
    void ShowView();
}

class ConfigurationPresenter : IConfigurationPresenter
{
    Configuration configuration = new Configuration();
    IConfigurationView view;

    public ConfigurationPresenter(IConfigurationView view)
    {
        this.view = view;            
        this.view.SelectConfigurationFile += delegate
        {
            // The ISelectFilePresenter and ISelectFileView behaviors
            // are implicit here, but in a WinForms case, a call to
            // OpenFileDialog wouldn't be too far fetched...
            var selectFilePresenter = Gimme.The<ISelectFilePresenter>();
            selectFilePresenter.ShowView();
            this.configuration.FullPath = selectFilePresenter.FullPath;
            this.view.SetConfigurationFile(this.configuration.FullPath);
        };
    }

    public void ShowView()
    {
        this.view.SetConfigurationFile(this.configuration.FullPath);
        this.view.Show();
    }
}

En plus de ce qui précède, j'ai généralement une IViewinterface de base dans laquelle je cache la Show()vue du propriétaire ou le titre de vue dont mes vues bénéficient généralement.

À vos questions:

1. Lorsque le winform se charge, il doit obtenir une arborescence. Ai-je raison de penser que la vue devrait donc appeler une méthode telle que: presenter.gettree (), celle-ci déléguera à son tour au modèle, qui obtiendra les données de l'arborescence, la créera et la configurera, la renverra au présentateur, qui à son tour passera à la vue qui l'attribuera alors simplement à, disons, un panneau?

J'appellerais IConfigurationView.SetTreeData(...)de IConfigurationPresenter.ShowView(), juste avant l'appel àIConfigurationView.Show()

2. Est-ce que ce serait la même chose pour n'importe quel contrôle de données sur le Winform, car j'ai également un datagridview?

Oui, j'appellerais IConfigurationView.SetTableData(...)pour ça. C'est à la vue de formater les données qui lui sont données. Le présentateur obéit simplement au contrat de la vue selon lequel il veut des données tabulaires.

3. Mon application comporte un certain nombre de classes de modèles avec le même assemblage. Il prend également en charge une architecture de plugins avec des plugins qui doivent être chargés au démarrage. La vue appellerait-elle simplement une méthode de présentateur, qui à son tour appellerait une méthode qui charge les plugins et afficherait les informations dans la vue? Quel niveau contrôlerait alors les références du plugin. La vue contiendrait-elle des références à eux ou au présentateur?

Si les plugins sont liés à la vue, les vues doivent les connaître, mais pas le présentateur. S'ils concernent uniquement les données et le modèle, la vue ne devrait rien avoir à voir avec eux.

4. Ai-je raison de penser que la vue doit gérer tout ce qui concerne la présentation, de la couleur du nœud de l'arborescence à la taille de la grille de données, etc.?

Oui. Considérez-le comme le présentateur fournissant du XML qui décrit les données et la vue qui prend les données et leur applique une feuille de style CSS. Concrètement, le présentateur peut appeler IRoadMapView.SetRoadCondition(RoadCondition.Slippery)et la vue rend alors la route en rouge.

Qu'en est-il des données pour les nœuds cliqués?

5. Si, lorsque je clique sur les treenodes, devrais-je passer par le nœud spécifique au présentateur, puis à partir de là, le présentateur déterminerait les données dont il a besoin et demandait ensuite au modèle ces données, avant de les présenter à la vue?

Si possible, je transmettrais toutes les données nécessaires pour présenter l'arbre dans une vue en un seul coup. Mais si certaines données sont trop volumineuses pour être transmises depuis le début ou si elles sont de nature dynamique et ont besoin du «dernier instantané» du modèle (via le présentateur), alors j'ajouterais quelque chose comme event LoadNodeDetailsEventHandler LoadNodeDetailsl'interface de vue, de sorte que le Le présentateur peut s'y abonner, récupérer les détails du nœud dans LoadNodeDetailsEventArgs.Node(éventuellement via son identifiant quelconque) à partir du modèle, de sorte que la vue puisse mettre à jour les détails de son nœud affiché lorsque le délégué du gestionnaire d'événements revient. Notez que des modèles asynchrones peuvent être nécessaires si la récupération des données peut être trop lente pour une bonne expérience utilisateur.

Johann Gerell
la source
3
Je ne pense pas qu'il faille nécessairement dissocier la vue et le présentateur. Je dissocie généralement le modèle et le présentateur, le présentateur écoutant les événements du modèle et agissant en conséquence (mettre à jour la vue). Avoir un présentateur dans la vue facilite la communication entre la vue et le présentateur.
kasperhj
11
@lejon: Vous dites qu'avoir un présentateur dans la vue facilite la communication entre la vue et le présentateur , mais je ne suis pas du tout d' accord. Mon point de vue est le suivant: lorsque la vue connaît le présentateur, alors pour chaque événement de vue, la vue doit décider quelle méthode de présentateur est la bonne à appeler. C'est "2 points de complexité", puisque la vue ne sait pas vraiment quel événement de vue correspond à quelle méthode de présentateur . Le contrat ne le précise pas.
Johann Gerell
5
@lejon: Si, d'un autre côté, la vue expose uniquement l'événement réel, alors le présentateur lui-même (qui sait ce qu'il veut faire lorsqu'un événement de vue se produit) s'y abonne simplement pour faire la bonne chose. C'est seulement "1 point de complexité", ce qui dans mon livre est deux fois plus bon que "2 points de complexité". De manière générale, moins de couplage signifie moins de coûts de maintenance pendant toute la durée d'un projet.
Johann Gerell
9
J'ai moi aussi tendance à utiliser le présentateur encapsulé comme expliqué dans ce lien lostechies.com/derekgreer/2008/11/23/… dans lequel la vue est le seul titulaire du présentateur.
ak3nat0n
3
@ ak3nat0n: En ce qui concerne les trois styles de MVP expliqués dans le lien que vous avez fourni, je pense que cette réponse de Johann pourrait être plus étroitement alignée avec le troisième style qui est nommé Observing Presenter Style : "L'avantage du style Observing Presenter est que il dissocie complètement la connaissance du présentateur de la vue, ce qui rend la vue moins sensible aux modifications dans le présentateur. "
DavidRR
11

Le présentateur, qui contient toute la logique de la vue, doit répondre au bouton cliqué comme le dit @JochemKempe . En termes pratiques, le gestionnaire d'événements de clic de bouton appelle presenter.OpenFile(). Le présentateur est alors en mesure de déterminer ce qui doit être fait.

S'il décide que l'utilisateur doit sélectionner un fichier, il rappelle la vue (via une interface de vue) et laisse la vue, qui contient toutes les caractéristiques techniques de l'interface utilisateur, afficher le fichier OpenFileDialog. Il s'agit d'une distinction très importante en ce que le présentateur ne doit pas être autorisé à effectuer des opérations liées à la technologie d'interface utilisateur utilisée.

Le fichier sélectionné sera ensuite renvoyé au présentateur qui poursuit sa logique. Cela peut impliquer n'importe quel modèle ou service doit gérer le traitement du fichier.

La principale raison d'utiliser un modèle MVP, imo, est de séparer la technologie d'interface utilisateur de la logique de vue. Ainsi, le présentateur orchestre toute la logique tandis que la vue la sépare de la logique de l'interface utilisateur. Cela a le très bel effet secondaire de rendre le présentateur entièrement testable à l'unité.

Mise à jour: puisque le présentateur est l'incarnation de la logique trouvée dans une vue spécifique , la relation vue-présentateur est IMO une relation un-à-un. Et à toutes fins pratiques, une instance de vue (par exemple un formulaire) interagit avec une instance de présentateur et une instance de présentateur interagit avec une seule instance de vue.

Cela dit, dans ma mise en œuvre de MVP avec WinForms, le présentateur interagit toujours avec la vue via une interface représentant les capacités de l'interface utilisateur de la vue. Il n'y a aucune limitation sur la vue implémentant cette interface, ainsi différents "widgets" peuvent implémenter la même interface de vue et réutiliser la classe de présentateur.

Peter Lillevold
la source
Merci. Donc, dans la méthode presenter.OpenFile (), il ne devrait pas avoir le code pour afficher l'openfiledialog? Au lieu de cela, il devrait revenir dans la vue pour que cette fenêtre s'affiche?
Darren Young le
4
Oui, je ne laisserais jamais le présentateur ouvrir directement les boîtes de dialogue, car cela briserait vos tests. Soit déchargez cela sur la vue, soit, comme je l'ai fait dans certains scénarios, demandez à une classe «FileOpenService» distincte de gérer l'interaction de dialogue réelle. De cette façon, vous pouvez simuler le service d'ouverture de fichiers pendant les tests. Mettre un tel code dans un service séparé peut vous donner de beaux effets secondaires de réutilisation :)
Peter Lillevold
2

Le présentateur doit agir à la fin de la demande et afficher la fenêtre openfiledialog comme vous l'avez suggéré. Étant donné qu'aucune donnée du modèle n'est requise, le présentateur peut et doit traiter la demande.

Supposons que vous ayez besoin des données pour créer des entités dans votre modèle. Vous pouvez soit transmettre le flux à la couche d'accès où vous avez une méthode pour créer des entités à partir du flux, mais je vous suggère de gérer l'analyse du fichier dans votre présentateur et d'utiliser un constructeur ou une méthode Create par entité dans votre modèle.

JochemKempe
la source
1
Merci pour la réponse. De plus, auriez-vous un seul présentateur pour la vue? Et ce présentateur gère la demande ou, si des données sont requises, délègue-t-il à n'importe quel nombre de classes de modèle qui agissent sur les demandes spécifiques? Est-ce la bonne manière? Merci encore.
Darren Young
3
Une vue a un présentateur, mais un présentateur peut avoir plusieurs vues.
JochemKempe