Comment bien structurer un projet en Winform?

26

Il y a quelque temps, j'ai commencé à créer une application Winform et à l'époque elle était petite et je n'ai pas réfléchi à la manière de structurer le projet.

Depuis lors, j'ai ajouté des fonctionnalités supplémentaires selon mes besoins et le dossier du projet devient de plus en plus gros et maintenant je pense qu'il est temps de structurer le projet d'une manière ou d'une autre, mais je ne sais pas quelle est la bonne façon, donc j'ai quelques questions.

Comment restructurer correctement le dossier du projet?

En ce moment, je pense à quelque chose comme ça:

  • Créer un dossier pour les formulaires
  • Créer un dossier pour les classes utilitaires
  • Créer un dossier pour les classes contenant uniquement des données

Quelle est la convention de dénomination lors de l'ajout de classes?

Dois-je également renommer des classes afin que leurs fonctionnalités puissent être identifiées en regardant simplement leur nom? Par exemple, renommer toutes les classes de formulaires, de sorte que leur nom se termine par Form . Ou n'est-ce pas nécessaire si des dossiers spéciaux sont créés pour eux?

Que faire pour que tout le code du formulaire principal ne se retrouve pas dans Form1.cs

Un autre problème que j'ai rencontré est que, comme le formulaire principal devient plus massif avec chaque fonctionnalité que j'ajoute, le fichier de code (Form1.cs) devient vraiment gros. J'ai par exemple un TabControl et chaque onglet a un tas de contrôles et tout le code s'est retrouvé dans Form1.cs. Comment éviter cela?

De plus, connaissez-vous des articles ou des livres traitant de ces problèmes?

user850010
la source

Réponses:

24

Il semble que vous soyez tombé dans certains des pièges courants, mais ne vous inquiétez pas, ils peuvent être corrigés :)

Vous devez d'abord regarder votre application un peu différemment et commencer à la décomposer en morceaux. Nous pouvons diviser les morceaux dans deux directions. Tout d'abord, nous pouvons séparer la logique de contrôle (les règles métier, le code d'accès aux données, le code des droits utilisateur, tout ce genre de choses) du code de l'interface utilisateur. Deuxièmement, nous pouvons décomposer le code de l'interface utilisateur en morceaux.

Nous ferons donc la dernière partie en premier, en décomposant l'interface utilisateur en morceaux. La façon la plus simple de le faire est d'avoir un seul formulaire hôte sur lequel vous composez votre interface utilisateur avec des commandes utilisateur. Chaque contrôle utilisateur sera en charge d'une région du formulaire. Imaginez donc que votre application contienne une liste d'utilisateurs, et lorsque vous cliquez sur un utilisateur, une zone de texte en dessous est remplie avec ses détails. Vous pouvez avoir un contrôle utilisateur gérant l'affichage de la liste des utilisateurs et un second contrôlant l'affichage des détails de l'utilisateur.

Le vrai truc ici est de savoir comment gérer la communication entre les commandes. Vous ne voulez pas que 30 contrôles utilisateur sur le formulaire contiennent tous des références les uns aux autres et appellent des méthodes dessus.

Vous créez donc une interface pour chaque contrôle. L'interface contient les opérations que le contrôle accepte et tous les événements qu'il déclenche. Lorsque vous pensez à cette application, vous ne vous souciez pas si la sélection de liste de zone de liste change, vous êtes intéressé par le fait qu'un nouvel utilisateur a changé.

Donc, en utilisant notre exemple d'application, la première interface pour le contrôle hébergeant la liste des utilisateurs inclurait un événement appelé UserChanged qui passe un objet utilisateur.

C'est génial parce que maintenant, si vous vous ennuyez de la liste et que vous voulez un contrôle de l'œil magique zoomy 3d, il vous suffit de le coder sur la même interface et de le brancher :)

Ok, donc la deuxième partie, séparant la logique de l'interface utilisateur de la logique du domaine. Eh bien, c'est un chemin bien usé et je vous recommande de regarder le modèle MVP ici. C'est vraiment simple.

Chaque contrôle est maintenant appelé une vue (V dans MVP) et nous avons déjà couvert la plupart de ce qui est nécessaire ci-dessus. Dans ce cas, le contrôle et une interface pour cela.

Tout ce que nous ajoutons, c'est le modèle et le présentateur.

Le modèle contient la logique qui gère l'état de votre application. Vous savez, il irait à la base de données pour obtenir les utilisateurs, écrire dans la base de données lorsque vous ajoutez un utilisateur, etc. L'idée est que vous pouvez tester tout cela en toute isolation de tout le reste.

Le présentateur est un peu plus difficile à expliquer. C'est une classe qui se situe entre le modèle et la vue. Il est créé par la vue et la vue se transmet au présentateur à l'aide de l'interface dont nous avons parlé précédemment.

Le présentateur n'a pas besoin d'avoir sa propre interface, mais j'aime quand même en créer une. Rend explicite ce que vous voulez que le présentateur fasse.

Ainsi, le présentateur exposerait des méthodes telles que ListOfAllUsers que la vue utiliserait pour obtenir sa liste d'utilisateurs, sinon, vous pourriez mettre une méthode AddUser la vue et l'appeler à partir du présentateur. Je préfère ce dernier. De cette façon, le présentateur peut ajouter un utilisateur à la liste quand il le souhaite.

Le présentateur aurait également des propriétés comme CanEditUser, qui retourneront true si l'utilisateur sélectionné peut être modifié. La vue interroge ensuite chaque fois qu'elle a besoin de savoir. Vous voudrez peut-être des éditables en noir et en lecture seule en gris. Techniquement, c'est une décision pour la vue car elle est axée sur l'interface utilisateur, si l'utilisateur est modifiable en premier lieu, c'est pour le présentateur. Le présentateur le sait parce qu'il parle au modèle.

Donc, en résumé, utilisez MVP. Microsoft fournit quelque chose appelé SCSF (Smart Client Software Factory) qui utilise MVP de la manière que j'ai décrite. Il fait aussi beaucoup d'autres choses. C'est assez complexe et je n'aime pas la façon dont ils font tout, mais cela peut aider.

Ian
la source
8

Personnellement, je préfère séparer différents domaines de préoccupation entre plusieurs assemblys au lieu de tout regrouper en un seul exécutable.

En règle générale, je préfère conserver une quantité minimale absolue de code dans le point d'entrée de l'application - Aucune logique métier, aucun code GUI et aucun accès aux données (bases de données / accès aux fichiers / connexions réseau / etc.); Je limite généralement le code du point d'entrée (c'est-à-dire l'exécutable) à quelque chose dans le sens de

  • Création et initialisation des différents composants d'application à partir de tous les assemblages dépendants
  • Configuration de tout composant tiers dont l'application entière dépend (par exemple Log4Net pour la sortie de diagnostic)
  • Je vais aussi probablement inclure un bit de code de type "attraper toutes les exceptions et enregistrer la trace de la pile" dans la fonction principale qui aidera à consigner les circonstances de toute défaillance critique / fatale imprévue.

Quant aux composants d'application eux-mêmes, je vise généralement au moins trois dans une petite application

  • Couche d'accès aux données (connexions à la base de données, accès aux fichiers, etc.) - selon la complexité des données persistantes / stockées utilisées par l'application, il peut y avoir plusieurs de ces assemblages - je créerais probablement un assemblage distinct pour la gestion de la base de données (éventuellement même multiple les assemblages si l'interaction avec la base de données impliquait quelque chose de complexe - par exemple, si vous êtes stucx avec une base de données mal conçue, vous devrez peut-être gérer les relations de base de données dans le code, donc il pourrait être judicieux d'écrire plusieurs modules pour l'insertion et la récupération)

  • Logic Layer - la "viande" principale contenant toutes les décisions et algorithmes qui font fonctionner votre application. Ces décisions ne devraient absolument rien savoir de l'interface graphique (qui dit qu'il y a une interface graphique?), Et ne devraient absolument rien savoir de la base de données (Hein? Il y a une base de données? Pourquoi pas un fichier?). Il est à espérer qu'une couche logique bien conçue peut être «arrachée» et déposée dans une autre application sans avoir besoin d'être recompilée. Dans une application compliquée, il peut y avoir tout un tas de ces assemblages logiques (car vous voudrez peut-être simplement extraire des `` morceaux '' sans faire glisser le reste de l'application)

  • Couche de présentation (c'est-à-dire l'interface graphique); Dans une petite application, il peut y avoir un seul "formulaire principal" avec quelques boîtes de dialogue qui peuvent toutes aller dans un seul assemblage - dans une application plus grande, il peut y avoir des assemblages séparés pour des parties fonctionnelles entières de l'interface graphique. Les classes ici ne feront guère plus que faire fonctionner l'interaction utilisateur - ce sera un peu plus qu'un shell avec une validation d'entrée de base, la gestion de toute animation, etc. Tout événement / clic de bouton qui "fait quelque chose" sera transmis à la couche logique (donc ma couche de présentation ne contiendra strictement aucune logique d'application, mais elle ne placera pas non plus la charge de tout code GUI sur la couche logique - de sorte que toutes les barres de progression ou autres éléments de fantaisie seront également assis dans l'assemblage de présentation / ies)

Ma principale justification pour diviser les couches Présentation, Logique et Données en assemblys séparés est la suivante: je pense qu'il est préférable de pouvoir exécuter la logique d'application principale sans votre base de données ou votre interface graphique.

Pour le dire autrement; si je veux écrire un autre exécutable qui se comporte exactement de la même manière que votre application, mais utilise une interface de ligne de commande ou une interface Web; et permute le stockage de la base de données pour le stockage de fichiers (ou un autre type de base de données peut-être), alors je peux le faire sans avoir à toucher à la logique principale de l'application - tout ce que je dois faire est d'écrire un petit outil en ligne de commande et un autre modèle de données, puis "tout brancher ensemble", et je suis prêt à partir.

Vous pensez peut-être "Eh bien, je ne vais jamais vouloir faire quoi que ce soit, donc peu importe si je ne peux pas échanger ces choses" - le vrai problème est que l'une des caractéristiques d'une application modulaire est la possibilité d'extraire des «morceaux» (sans avoir besoin de recompiler quoi que ce soit), et de réutiliser ces morceaux ailleurs. Pour écrire du code comme celui-ci, cela vous oblige généralement à réfléchir longuement aux principes de conception - Vous devrez penser à écrire beaucoup plus d'interfaces et réfléchir soigneusement aux compromis de divers principes SOLID (dans le même comme vous le feriez pour Behavior-Driven-Development ou TDD)

Parfois, cette séparation d'un bloc de code monolithique existant est un peu pénible et nécessite beaucoup de refactorisation soigneuse - c'est OK, vous devriez pouvoir le faire de manière incrémentielle - vous pouvez même atteindre un point où il y a trop d'assemblys et vous décidez revenir en arrière et recommencer à boucler les choses (aller trop loin dans la direction opposée peut avoir pour effet de rendre ces assemblages plutôt inutiles par eux-mêmes)

Ben Cottrell
la source
4

Selon la structure des dossiers, ce que vous avez suggéré est OK en général. Vous devrez peut-être ajouter des dossiers pour les ressources (parfois, les gens créent des ressources telles que chaque ensemble de ressources est regroupé sous un code de langue ISO pour prendre en charge plusieurs langues), des images, des scripts de base de données, des préférences utilisateur (si elles ne sont pas traitées comme des ressources), des polices , DLL externes, DLL locales, etc.

Quelle est la convention de dénomination lors de l'ajout de classes?

Bien sûr, vous souhaitez séparer chaque classe en dehors du formulaire. Je recommanderais également un fichier par classe (bien que MS ne le fasse pas dans le code généré pour EF par exemple).

Beaucoup de gens utilisent des noms courts significatifs au pluriel (par exemple Clients). Certains préfèrent que le nom soit proche du nom singulier de la table de base de données correspondante (si vous utilisez le mappage 1-1 entre les objets et les tables).

Pour les classes de dénomination, il existe de nombreuses sources, par exemple, consultez: Conventions de dénomination .net et normes de programmation - Meilleures pratiques et / ou directives de codage STOVF-C #

Que faire pour que tout le code du formulaire principal ne se retrouve pas dans Form1.cs

Le code d'assistance doit aller dans une ou plusieurs classes d'assistance. D'autres codes très communs aux contrôles GUI, tels que l'application de préférences utilisateur à une grille, peuvent être supprimés du formulaire et ajoutés aux classes d'assistance ou en sous-classant le contrôle en question et en y créant les méthodes nécessaires.

En raison de la nature action-événementielle des formulaires MS Windows, il n'y a rien que je sache qui pourrait vous aider à retirer le code du formulaire principal sans ajouter d'ambiguïté et d'effort. Cependant, MVVM peut être un choix (dans les projets futurs), voir par exemple: MVVM pour Windows Forms .

Aucune chance
la source
2

Considérez MVP comme une option car cela vous aidera à organiser la logique de présentation, qui est tout dans les grandes applications commerciales. Dans la vie réelle, la logique de l'application réside principalement dans la base de données, donc vous avez rarement besoin d'écrire réellement une couche métier en code natif, ne la laissant qu'avec le besoin d'avoir une fonctionnalité de présentation bien structurée.

La structure de type MVP se traduira par un ensemble de présentateurs, ou de contrôleurs si vous préférez, qui se coordonneront les uns avec les autres et ne se mélangeront pas avec l'interface utilisateur ou le code derrière les choses. Vous pouvez même utiliser différentes interfaces utilisateur avec le même contrôleur.

Enfin, des ressources d'organisation simples, le découplage des composants et la spécification des dépendances avec IoC et DI, ainsi qu'une approche MVP vous fourniront les clés pour construire un système qui évite les erreurs et la complexité courantes, est livré à temps et est ouvert aux changements.

Panos Roditakis
la source
1

La structure d'un projet dépend totalement du projet et de sa taille, cependant vous pouvez ajouter quelques dossiers par exemple

  • Commun (contenant des classes, par exemple des utilitaires)
  • DataAccess (classes liées à l'accès aux données à l'aide de SQL ou de tout autre serveur de base de données que vous utilisez)
  • Styles (si vous avez des fichiers CSS dans votre projet)
  • Ressources (par exemple, images, fichiers de ressources)
  • WorkFlow (classes liées au workflow si vous en avez)

Vous n'avez pas besoin de mettre les formulaires dans n'importe quel type de dossier, renommez simplement vos formulaires en conséquence. Je veux dire son bon sens, personne ne sait quel nom devrait être vos formes mieux que vous-même.

La convention de dénomination est comme si votre classe imprimait un message "Hello World", alors le nom de classe devrait être quelque chose lié à la tâche et le nom approprié de la classe devrait HelloWorld.cs.

vous pouvez également créer des régions, par exemple

#region Hello puis endregionà la fin.

Vous pouvez créer des classes pour les onglets, j'en suis presque sûr, et une dernière chose est de réutiliser votre code lorsque cela est possible, la meilleure pratique consiste à créer des méthodes et à les réutiliser si nécessaire.

Livres ? euh.

Il n'y a pas de livres qui vous disent la structure des projets car chaque projet est différent, vous apprenez ce genre de choses par expérience.

J'espère que cela a aidé!

Muhammad Raja
la source