Come strutturare correttamente un progetto in winform?


26

Qualche tempo fa ho iniziato a creare un'applicazione winform e in quel momento era piccola e non ho pensato a come strutturare il progetto.

Da allora ho aggiunto funzionalità aggiuntive di cui avevo bisogno e la cartella del progetto sta diventando sempre più grande e ora penso che sia il momento di strutturare il progetto in qualche modo, ma non sono sicuro di quale sia il modo corretto, quindi ho alcune domande.

Come ristrutturare correttamente la cartella del progetto?

Al momento sto pensando a qualcosa del genere:

  • Crea cartella per moduli
  • Crea cartella per classi di utilità
  • Crea una cartella per le classi che contengono solo dati

Qual è la convenzione di denominazione quando si aggiungono le classi?

Dovrei anche rinominare le classi in modo che la loro funzionalità possa essere identificata solo guardando il loro nome? Ad esempio, rinominando tutte le classi di moduli, in modo che il loro nome termini con Modulo . O questo non è necessario se vengono create cartelle speciali per loro?

Cosa fare, in modo che non tutto il codice per il modulo principale finisca in Form1.cs

Un altro problema che ho riscontrato è che mentre il modulo principale sta diventando sempre più massiccio con ogni funzione che aggiungo, il file di codice (Form1.cs) sta diventando davvero grande. Ho ad esempio un TabControl e ogni scheda ha un sacco di controlli e tutto il codice è finito in Form1.cs. Come evitarlo?

Inoltre, conosci qualche articolo o libro che affronta questi problemi?

Risposte:


24

Sembra che tu sia caduto in alcune delle insidie ​​comuni, ma non preoccuparti, possono essere risolti :)

Per prima cosa devi guardare la tua applicazione in modo leggermente diverso e iniziare a scomporla in blocchi. Possiamo dividere i pezzi in due direzioni. Innanzitutto possiamo separare la logica di controllo (le regole aziendali, il codice di accesso ai dati, il codice dei diritti utente, tutto quel genere di cose) dal codice dell'interfaccia utente. In secondo luogo, possiamo suddividere il codice dell'interfaccia utente in blocchi.

Quindi faremo prima l'ultima parte, suddividendo l'interfaccia utente in blocchi. Il modo più semplice per farlo è quello di avere un unico modulo host su cui componi l'interfaccia utente con controlli utente. Ogni controllo utente sarà responsabile di una regione del modulo. Immagina quindi che la tua applicazione avesse un elenco di utenti e quando fai clic su un utente una casella di testo sotto viene riempita con i suoi dettagli. Si potrebbe avere un controllo utente che gestisce la visualizzazione dell'elenco utenti e un secondo che gestisce la visualizzazione dei dettagli dell'utente.

Il vero trucco qui è come gestire la comunicazione tra i controlli. Non si desidera 30 controlli utente nel modulo, tutti in modo casuale con riferimenti reciproci e metodi di chiamata su di essi.

Quindi crei un'interfaccia per ogni controllo. L'interfaccia contiene le operazioni che il controllo accetterà e tutti gli eventi che genera. Quando pensi a questa app, non ti importa se la selezione dell'elenco della casella di riepilogo cambia, sei interessato al fatto che un nuovo utente è cambiato.

Quindi, usando la nostra app di esempio, la prima interfaccia per il controllo che ospita la casella di riepilogo degli utenti includerebbe un evento chiamato UserChanged che trasmette un oggetto utente.

Questo è fantastico perché ora se ti annoi della casella di riepilogo e desideri un controllo 3D Magic Eye zoomy, basta codificarlo sulla stessa interfaccia e collegarlo :)

Ok, quindi parte seconda, separando la logica dell'interfaccia utente dalla logica del dominio. Bene, questo è un percorso ben usurato e ti consiglio di guardare qui il modello MVP. È davvero semplice

Ogni controllo è ora chiamato View (V in MVP) e abbiamo già coperto la maggior parte di ciò che è necessario sopra. In questo caso, il controllo e un'interfaccia per esso.

Tutto ciò che stiamo aggiungendo è il modello e il presentatore.

Il modello contiene la logica che gestisce lo stato dell'applicazione. Sai tutto, andrebbe nel database per ottenere gli utenti, scrivere nel database quando aggiungi un utente e così via. L'idea è che puoi testare tutto questo in completo isolamento da tutto il resto.

Il presentatore è un po 'più complicato da spiegare. È una classe che si trova tra il modello e la vista. Viene creato dalla vista e la vista si passa al presentatore usando l'interfaccia di cui abbiamo discusso in precedenza.

Il presentatore non deve avere una propria interfaccia, ma mi piace crearne uno comunque. Rende esplicito ciò che vuoi che il presentatore faccia.

Quindi il presentatore esponerebbe metodi come ListOfAllUsers che la vista userebbe per ottenere il suo elenco di utenti, in alternativa, potresti mettere un metodo AddUser la vista e chiamarlo dal presentatore. Preferisco quest'ultima. In questo modo il relatore può aggiungere un utente alla casella di riepilogo ogni volta che lo desidera.

Il Presenter avrebbe anche proprietà come CanEditUser, che tornerà vero se l'utente selezionato può essere modificato. La vista quindi lo interrogherà ogni volta che deve saperlo. Potresti volere quelli modificabili in nero e leggere solo quelli in grigio. Tecnicamente questa è una decisione per la vista in quanto focalizzata sull'interfaccia utente, se l'utente è modificabile in primo luogo è per il presentatore. Il presentatore lo sa perché parla al Modello.

Quindi, in sintesi, utilizzare MVP. Microsoft fornisce qualcosa chiamato SCSF (Smart Client Software Factory) che utilizza MVP nel modo che ho descritto. Fa anche molte altre cose. È abbastanza complesso e non mi piace il modo in cui fanno tutto, ma può aiutare.


8

Personalmente preferisco separare le diverse aree di preoccupazione tra più assiemi anziché raggruppare tutto in un unico eseguibile.

In genere, preferisco mantenere una quantità minima assoluta di codice nel punto di ingresso dell'applicazione - Nessuna logica aziendale, nessun codice GUI e nessun accesso ai dati (database / accesso ai file / connessioni di rete / ecc.); Di solito limito il codice del punto di ingresso (ovvero l'eseguibile) a qualcosa lungo le righe di

  • Creazione e inizializzazione dei vari componenti dell'applicazione da tutti i gruppi dipendenti
  • Configurazione di componenti di terze parti da cui dipende l'intera applicazione (ad es. Log4Net per l'output diagnostico)
  • inoltre includerò probabilmente un bit di codice "cattura tutte le eccezioni e registra la traccia dello stack" nella funzione principale che aiuterà a registrare le circostanze di eventuali guasti critici / fatali non previsti.

Per quanto riguarda gli stessi componenti dell'applicazione, in genere cerco almeno tre in una piccola applicazione

  • Livello di accesso ai dati (connessioni al database, accesso ai file, ecc.) - a seconda della complessità di eventuali dati persistenti / memorizzati utilizzati dall'applicazione, potrebbero esserci diversi di questi assiemi - Probabilmente creerei un assieme separato per la gestione del database (eventualmente anche più gli assiemi se l'interazione con il database implicava qualcosa di complesso - ad es. se si è bloccati con un database mal progettato, potrebbe essere necessario gestire le relazioni DB nel codice, quindi potrebbe avere senso scrivere più moduli per l'inserimento e il recupero)

  • Logic Layer: la "carne" principale contenente tutte le decisioni e gli algoritmi che fanno funzionare l'applicazione. Queste decisioni non dovrebbero sapere assolutamente nulla della GUI (chi dice che c'è una GUI?) E non dovrebbero assolutamente sapere nulla del database (eh? C'è un database? Perché non un file?). Si spera che un livello logico ben progettato possa essere "strappato" e rilasciato in un'altra applicazione senza bisogno di essere ricompilato. In un'applicazione complicata, potrebbero esserci un sacco di questi assemblaggi logici (perché potresti voler semplicemente strappare "pezzi" senza trascinare il resto dell'applicazione)

  • Livello di presentazione (ovvero la GUI); In una piccola applicazione, potrebbe esserci un solo "modulo principale" con un paio di finestre di dialogo che possono andare tutte in un singolo assieme - in un'applicazione più grande, potrebbero esserci assiemi separati per intere parti funzionali della GUI. Le classi qui faranno poco più che far funzionare l'interazione dell'utente - sarà poco più di una shell con qualche convalida di input di base, gestione di qualsiasi animazione, ecc. Eventuali clic di eventi / pulsanti che "fanno qualcosa" verranno passati a il livello logico (quindi il mio livello di presentazione non conterrà assolutamente alcuna logica applicativa, ma non porrà nemmeno il peso di alcun codice GUI sul livello logico, quindi eventuali barre di avanzamento o altre cose fantasiose si troveranno anche nell'assieme di presentazione / i)

La mia logica principale per la suddivisione dei livelli Presentazione, Logica e Dati in assiemi separati è questa: ritengo sia preferibile poter eseguire la logica principale dell'applicazione senza il database o la GUI.

Per dirla in un altro modo; se voglio scrivere un altro eseguibile che si comporta esattamente allo stesso modo dell'applicazione, ma utilizza un'interfaccia a riga di comando o un'interfaccia Web; e scambia l'archiviazione del database con l'archiviazione dei file (o forse un diverso tipo di database), quindi posso farlo senza dover toccare affatto la logica dell'applicazione principale - tutto ciò che dovrei fare è scrivere un po 'uno strumento da riga di comando e un altro modello di dati, quindi "collegalo tutti insieme" e sono pronto per iniziare.

Si può pensare: "Beh ho mai intenzione di voler fare nulla di tutto ciò in modo che non importa se non riesco a scambiare queste cose intorno" - il vero punto è che uno dei tratti distintivi di un'applicazione modulare è la capacità di estrarre "blocchi" (senza necessità di ricompilare nulla) e riutilizzare quei blocchi altrove. Per scrivere codice in questo modo, in genere ti costringe a riflettere a lungo sui principi di progettazione - Dovrai pensare a scrivere molte più interfacce e riflettere attentamente sui compromessi di vari principi SOLID (nello stesso come faresti per Behavior-Driven-Development o TDD)

A volte ottenere questa separazione da un blocco monolitico di codice esistente è un po 'doloroso e richiede un sacco di attento refactoring - va bene, dovresti essere in grado di farlo in modo incrementale - potresti persino raggiungere un punto in cui ci sono troppi assembly e decidi tornare indietro e ricominciare a avvolgere le cose (andare troppo lontano nella direzione opposta può avere l'effetto di rendere quelle assemblee piuttosto inutili da sole)


4

Come per la struttura delle cartelle, ciò che hai suggerito è OK in generale. Potrebbe essere necessario aggiungere cartelle per le risorse (a volte le persone creano risorse in modo tale che ogni set di risorse sia raggruppato in un codice di lingua ISO per supportare diverse lingue), immagini, script di database, preferenze dell'utente (se non trattate come risorse), caratteri , DLL esterne, DLL locali, ecc.

Qual è la convenzione di denominazione quando si aggiungono le classi?

Ovviamente, vuoi separare ogni classe al di fuori del modulo. Vorrei anche raccomandare un file per classe (anche se MS non lo fa nel codice generato per esempio EF).

Molte persone usano nomi brevi significativi al plurale (ad es. Clienti). Alcuni ritengono che il nome sia vicino al nome singolare per la tabella del database corrispondente (se si utilizza la mappatura 1-1 tra oggetti e tabelle).

Per le classi di denominazione ci sono molte fonti, ad esempio dare un'occhiata a: Convenzioni di denominazione .net e standard di programmazione - Best practice e / o Linee guida per la codifica STOVF-C #

Cosa fare, in modo che non tutto il codice per il modulo principale finisca in Form1.cs

Il codice helper dovrebbe andare a una o più classi helper. Altro codice molto comune ai controlli della GUI come l'applicazione delle preferenze dell'utente a una griglia, potrebbe essere rimosso dal modulo e aggiunto alle classi di supporto o classificando il controllo in questione e creando lì i metodi necessari.

A causa della natura evento-azione di MS Windows Forms, non so nulla che possa aiutarti a togliere il codice dal modulo principale senza aggiungere ambiguità e impegno. Tuttavia, MVVM può essere una scelta (nei progetti futuri), vedere ad esempio: MVVM per Windows Forms .


2

Considera MVP come un'opzione perché ti aiuterà a organizzare la logica di presentazione, che è tutto nelle grandi applicazioni aziendali. Nella vita reale, la logica delle app risiede principalmente nel database, quindi raramente hai la necessità di scrivere un livello aziendale nel codice nativo, lasciandolo solo con la necessità di avere funzionalità di presentazione ben strutturate.

Una struttura simile a MVP si tradurrà in un set di presentatori o controller, se preferisci, che si coordineranno tra loro e non si mescoleranno con l'interfaccia utente o il codice dietro roba. È anche possibile utilizzare interfacce utente diverse con lo stesso controller.

Infine, la semplice organizzazione delle risorse, il disaccoppiamento dei componenti e la specifica delle dipendenze con IoC e DI, insieme a un approccio MVP, ti fornirà le chiavi per la costruzione di un sistema che evita errori e complessità comuni, viene consegnato in tempo ed è aperto alle modifiche.


1

La struttura di un progetto dipende totalmente dal Progetto e dalle sue dimensioni, tuttavia è possibile aggiungere alcune cartelle ad es

  • Comune (contenente classi ad es. Utilità)
  • DataAccess (classi relative all'accesso ai dati tramite sql o qualsiasi altro server di database in uso)
  • Stili (se hai dei file CSS nel tuo progetto)
  • Risorse (ad es. Immagini, file di risorse)
  • WorkFlow (Classi correlate al flusso di lavoro se ne hai)

Non è necessario inserire i moduli in alcun tipo di cartella, basta rinominare i moduli di conseguenza. Intendo il suo buon senso, nessuno sa quale nome dovrebbe essere la tua forma meglio di te stesso.

La convenzione di denominazione è come se la tua classe stampasse un messaggio "Hello World", quindi il nome della classe dovrebbe essere correlato all'attività e il nome appropriato della classe dovrebbe HelloWorld.cs.

puoi anche creare regioni, ad es

#region Hello e poi endregionalla fine.

Puoi creare classi per le schede, sono abbastanza sicuro che puoi, e un'ultima cosa è riutilizzare il tuo codice dove possibile, la migliore pratica è quella di creare metodi e riutilizzarli dove necessario.

Libri? ehm.

Non ci sono libri che ti descrivono la struttura dei progetti poiché ogni progetto è diverso, impari questo tipo di cose per esperienza.

Spero che abbia aiutato!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.