Meilleure conception pour les formulaires Windows qui partageront des fonctionnalités communes


20

Dans le passé, j'ai utilisé l'héritage pour permettre l'extension des formulaires Windows dans mon application. Si tous mes formulaires avaient des contrôles, des illustrations et des fonctionnalités communs, je créerais un formulaire de base implémentant les contrôles et les fonctionnalités communs, puis autoriserais d'autres contrôles à hériter de ce formulaire de base. Cependant, j'ai rencontré quelques problèmes avec cette conception.

  1. Les contrôles ne peuvent être que dans un seul conteneur à la fois, donc tous les contrôles statiques dont vous disposez seront délicats. Par exemple: Supposons que vous disposiez d'un formulaire de base appelé BaseForm qui contenait un TreeView que vous rendez protégé et statique afin que toutes les autres instances (dérivées) de cette classe puissent modifier et afficher le même TreeView. Cela ne fonctionnerait pas pour plusieurs classes héritant de BaseForm, car TreeView ne peut être que dans un seul conteneur à la fois. Ce serait probablement sur le dernier formulaire initialisé. Bien que chaque instance puisse modifier le contrôle, il ne s'afficherait que dans un à un moment donné. Bien sûr, il existe des solutions de contournement, mais elles sont toutes laides. (Cela me semble être une très mauvaise conception. Pourquoi plusieurs conteneurs ne peuvent-ils pas stocker des pointeurs vers le même objet? Quoi qu'il en soit, c'est ce que c'est.)

  2. Indiquez entre les formulaires, c'est-à-dire les états des boutons, le texte des étiquettes, etc., je dois utiliser des variables globales et réinitialiser les états lors du chargement.

  3. Ce n'est pas vraiment très bien pris en charge par le concepteur de Visual Studio.

Existe-t-il une conception meilleure, mais toujours facile à entretenir? Ou l'héritage de forme est-il toujours la meilleure approche?

Mise à jour Je suis passé de MVC à MVP au modèle d'observateur au modèle d'événement. Voici ce que je pense pour le moment, veuillez critiquer:

Ma classe BaseForm contiendra uniquement les contrôles et les événements connectés à ces contrôles. Tous les événements qui nécessitent une sorte de logique pour les gérer passeront immédiatement à la classe BaseFormPresenter. Cette classe gérera les données de l'interface utilisateur, effectuera toutes les opérations logiques, puis mettra à jour le BaseFormModel. Le modèle exposera les événements, qui se déclencheront lors des changements d'état, à la classe Presenter, à laquelle il souscrira (ou observera). Lorsque le présentateur reçoit la notification d'événement, il exécute toute logique, puis le présentateur modifie la vue en conséquence.

Il n'y aura qu'une seule de chaque classe Model en mémoire, mais il pourrait y avoir de nombreuses instances de BaseForm et donc de BaseFormPresenter. Cela résoudrait mon problème de synchronisation de chaque instance de BaseForm avec le même modèle de données.

Des questions:

Quelle couche doit stocker des éléments comme le dernier bouton enfoncé, afin que je puisse le mettre en surbrillance pour l'utilisateur (comme dans un menu CSS) entre les formulaires?

Veuillez critiquer cette conception. Merci de votre aide!


Je ne vois pas pourquoi vous êtes obligé d'utiliser des variables globales, mais si oui, alors oui, il doit certainement y avoir une meilleure approche. Peut-être des usines pour créer des composants communs combinés avec la composition au lieu de l'héritage?
stijn

Votre conception entière est imparfaite. Si vous comprenez déjà que ce que vous voulez faire n'est pas pris en charge par Visual Studio, cela devrait vous dire quelque chose.
Ramhound

1
@Ramhound Il est supporté par Visual Studio, mais pas bien. C'est ainsi que Microsoft vous dit de le faire. Je trouve juste que c'est une douleur dans A. msdn.microsoft.com/en-us/library/aa983613(v=vs.71).aspx Quoi qu'il en soit, si vous avez une meilleure idée, je suis tous les yeux.
Jonathan Henson

@stijn Je suppose que je pourrais avoir une méthode dans chaque forme de base qui chargera les états de contrôle dans une autre instance. ie LoadStatesToNewInstance (instance BaseForm). Je pourrais l'appeler chaque fois que je veux montrer une nouvelle forme de ce type de base. form_I_am_about_to_hide.LoadStatesToNewInstance (this);
Jonathan Henson

Je ne suis pas un développeur .net, pouvez-vous développer le point numéro 1? Je pourrais avoir une solution
Imran Omar Bukhsh

Réponses:


6
  1. Je ne sais pas pourquoi vous avez besoin de contrôles statiques. Vous savez peut-être quelque chose que je ne sais pas. J'ai utilisé beaucoup d'héritage visuel mais je n'ai jamais vu de contrôles statiques nécessaires. Si vous avez un contrôle d'arborescence commun, laissez chaque instance de formulaire avoir sa propre instance du contrôle et partagez une seule instance des données liées aux arborescences.

  2. Le partage de l'état de contrôle (par opposition aux données) entre les formulaires est également une exigence inhabituelle. Êtes-vous sûr que FormB a vraiment besoin de connaître l'état des boutons sur FormA? Tenez compte des conceptions MVP ou MVC. Considérez chaque formulaire comme une "vue" stupide qui ne sait rien des autres vues ni même de l'application elle-même. Supervisez chaque vue avec un présentateur / contrôleur intelligent. Si cela a du sens, un seul présentateur peut superviser plusieurs vues. Associez un objet d'état à chaque vue. Si vous avez un état qui doit être partagé entre les vues, laissez le (s) présentateur (s) en assurer la médiation (et envisagez la liaison de données - voir ci-dessous).

  3. D'accord, Visual Studio vous donnera des maux de tête. Lorsque vous envisagez l'héritage de formulaire ou de contrôle d'utilisateur, vous devez soigneusement peser les avantages par rapport au coût potentiel (et probable) de la lutte avec les caprices et les limites frustrantes du concepteur de formulaire. Je suggère de garder l'héritage de forme au minimum - utilisez-le uniquement lorsque le gain est élevé. Gardez à l'esprit que, comme alternative au sous-classement, vous pouvez créer un formulaire "de base" commun et l'instancier simplement une fois pour chaque "enfant" potentiel, puis le personnaliser à la volée. Cela est logique lorsque les différences entre chaque version du formulaire sont mineures par rapport aux aspects partagés. (IOW: forme de base complexe, formes enfants seulement un peu plus complexes)

Utilisez les contrôles utilisateur lorsqu'ils vous aident à éviter une duplication importante du développement de l'interface utilisateur. Envisagez l'héritage de l'usercontrol mais appliquez les mêmes considérations que pour l'héritage de formulaire.

Je pense que le conseil le plus important que je puisse offrir est, si vous n'utilisez pas actuellement une certaine forme de modèle de vue / contrôleur, je vous encourage fortement à le faire. Il vous oblige à apprendre et à apprécier les avantages du découpage en vrac et de la séparation des couches.

réponse à votre mise à jour

Quelle couche doit stocker des éléments comme le dernier bouton enfoncé, afin que je puisse le mettre en surbrillance pour l'utilisateur ...

Vous pouvez partager l'état entre les vues de la même manière que vous partageriez l'état entre un présentateur et sa vue. Créez une classe spéciale SharedViewState. Pour plus de simplicité, vous pouvez en faire un singleton, ou vous pouvez l'instancier dans le présentateur principal et le transmettre à toutes les vues (via leurs présentateurs) à partir de là. Lorsque l'état est associé à des contrôles, utilisez la liaison de données lorsque cela est possible. La plupart des Controlpropriétés peuvent être liées aux données. Par exemple, la propriété BackColor d'un Button peut être liée à une propriété de votre classe SharedViewState. Si vous effectuez cette liaison sur tous les formulaires qui ont des boutons identiques, vous pouvez mettre en surbrillance Button1 sur tous les formulaires simplement en définissant SharedViewState.Button1BackColor = someColor.

Si vous n'êtes pas familier avec la liaison de données WinForms, appuyez sur MSDN et effectuez une lecture. Ce n'est pas difficile. Renseignez INotifyPropertyChanged-vous et vous êtes à mi-chemin.

Voici une implémentation typique d'une classe viewstate avec la propriété Button1BackColor comme exemple:

public class SharedViewState : INotifyPropertyChanged
{
    // boilerplate INotifyPropertyChanged stuff
    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }

    // example of a property for data-binding
    private Color button1BackColor;
    public Color Button1BackColor
    {
        get { return button1BackColor; }
        set
        {
            if (value != button1BackColor)
            {
                button1BackColor = value;
                NotifyPropertyChanged("Button1BackColor");
            }
        }
    }
}

Merci pour votre réponse réfléchie. Connaissez-vous une bonne référence au modèle de vue / contrôleur?
Jonathan Henson

1
Je me souviens avoir pensé que c'était une assez bonne introduction: codebetter.com/jeremymiller/2007/05/22/…
Igby Largeman

veuillez voir ma mise à jour.
Jonathan Henson

@JonathanHenson: réponse mise à jour.
Igby Largeman

5

Je gère une nouvelle application WinForms / WPF basée sur le modèle MVVM et les contrôles de mise à jour . Cela a commencé comme WinForms, puis j'ai créé une version WPF en raison de l'importance marketing d'une interface utilisateur qui a l'air bien. Je pensais que ce serait une contrainte de conception intéressante de maintenir une application qui prend en charge deux technologies d'interface utilisateur complètement différentes avec le même code backend (modèles de vue et modèles), et je dois dire que je suis assez satisfait de cette approche.

Dans mon application, chaque fois que j'ai besoin de partager des fonctionnalités entre des parties de l'interface utilisateur, j'utilise des contrôles personnalisés ou des contrôles utilisateur (classes dérivées de WPF UserControl ou WinForms UserControl). Dans la version WinForms, il existe un UserControl à l'intérieur d'un autre UserControl à l'intérieur d'une classe dérivée de TabPage, qui se trouve enfin à l'intérieur d'un contrôle onglet sur le formulaire principal. La version WPF a en fait UserControls imbriqué un niveau plus profondément. À l'aide de contrôles personnalisés, vous pouvez facilement composer une nouvelle interface utilisateur à partir des UserControls que vous avez créés précédemment.

Puisque j'utilise le modèle MVVM, je mets autant de logique de programme que possible dans le ViewModel (y compris le modèle de présentation / navigation ) ou le modèle (selon que le code est lié à l'interface utilisateur ou non), mais puisque le même ViewModel est utilisé à la fois par une vue WinForms et une vue WPF, le ViewModel n'est pas autorisé à contenir du code conçu pour WinForms ou WPF ou qui interagit directement avec l'interface utilisateur; ce code DOIT aller dans la vue.

Toujours dans le modèle MVVM, les objets d'interface utilisateur doivent éviter d'interagir les uns avec les autres! Au lieu de cela, ils interagissent avec les modèles de vue. Par exemple, un TextBox ne demanderait pas à un ListBox proche quel élément est sélectionné; à la place, la zone de liste enregistrerait une référence à l'élément actuellement sélectionné quelque part dans le calque viewmodel et la TextBox interrogerait le calque viewmodel pour découvrir ce qui est sélectionné en ce moment. Cela résout votre problème de savoir quel bouton a été poussé dans un formulaire à partir d'un formulaire différent. Vous venez de partager un objet Modèle de navigation (qui fait partie de la couche de modèle d'affichage de l'application) entre les deux formulaires et placez une propriété dans cet objet qui représente le bouton qui a été enfoncé.

WinForms lui-même ne prend pas très bien en charge le modèle MVVM, mais Update Controls fournit sa propre approche unique MVVM en tant que bibliothèque qui se trouve au-dessus de WinForms.

À mon avis, cette approche de la conception du programme fonctionne très bien et je prévois de l'utiliser dans de futurs projets. Les raisons pour lesquelles cela fonctionne si bien sont (1) que Update Controls gère automatiquement les dépendances et (2) qu'il est généralement très clair de structurer votre code: tout le code qui interagit avec les objets d'interface utilisateur appartient à la vue, tout le code qui est Lié à l'interface utilisateur, mais ne doit PAS interagir avec les objets d'interface utilisateur appartient au ViewModel. Très souvent, vous diviserez votre code en deux parties, une partie pour la vue et une autre partie pour le ViewModel. J'ai eu quelques difficultés à concevoir des menus contextuels dans mon système, mais j'ai finalement trouvé un design pour cela aussi.

J'ai aussi déliré sur les contrôles de mise à jour sur mon blog . Cette approche prend cependant beaucoup de temps pour s'y habituer et dans les applications à grande échelle (par exemple, si vos zones de liste contiennent des milliers d'éléments), vous pouvez rencontrer des problèmes de performances en raison des limitations actuelles de la gestion automatique des dépendances.


3

Je vais y répondre même si vous avez déjà accepté une réponse.

Comme d'autres l'ont souligné, je ne comprends pas pourquoi vous avez dû utiliser quelque chose de statique; cela donne l'impression que vous avez fait quelque chose de très mal.

Quoi qu'il en soit, j'ai eu le même problème que vous: dans mon application WinForms, j'ai plusieurs formulaires qui partagent certaines fonctionnalités et certains contrôles. De plus, tous mes formulaires dérivent déjà d'un formulaire de base (appelons-le "MyForm") qui ajoute des fonctionnalités au niveau du framework (quelle que soit l'application). pratique cela ne fonctionne que tant que vos formulaires ne font rien de plus que "Bonjour tout le monde! - OK - Annuler".

Ce que j'ai fini par faire, c'est ceci: je garde ma classe de base commune "MyForm", qui est assez complexe, et je continue d'en tirer tous les formulaires de ma candidature. Cependant, je ne fais absolument rien dans ces formulaires, donc VS Forms Designer n'a aucun problème à les modifier. Ces formulaires se composent uniquement du code que le Concepteur de formulaires génère pour eux. Ensuite, j'ai une hiérarchie parallèle distincte d'objets que j'appelle "Surrogates" qui contiennent toutes les fonctionnalités spécifiques à l'application, comme l'initialisation des contrôles, la gestion des événements générés par le formulaire et les contrôles, etc. Il y a un à un correspondance entre les classes de substitution et les dialogues dans mon application: il existe une classe de substitution de base qui correspond à "MyForm", puis une autre classe de substitution est dérivée qui correspond à "MyApplicationForm",

Chaque substitut accepte comme paramètre au moment de la construction un type spécifique de formulaire, et il s'y attache en s'inscrivant à ses événements. Il délègue également la base, jusqu'à "MySurrogate" qui accepte un "MyForm". Ce substitut s'enregistre avec l'événement "Disposed" du formulaire, de sorte que lorsque le formulaire est détruit, le substitut invoque une dérogation sur lui-même afin que lui et tous ses descendants puissent effectuer le nettoyage. (Se désinscrire des événements, etc.)

Cela a bien fonctionné jusqu'à présent.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.