DESCRIZIONE
Questa soluzione mostra come sia possibile realizzare un applicazione a moduli separati (DLL) indipendenti tra loro in cui ognuno dei quali possa assolvere ad un preciso compito. Un'elenco più dettagliato lo trovate nella sezione VANTAGGI, più sotto.
Premetto che il progetto di partenza non è opera mia ma di Luciano Bastianello, un veterano della community Visual Basic Tips&Tricks, purtroppo deceduto .
Chi lo conosce, sa che la sua grande esperienza e professionalità è indiscussa. Mi sono permesso umilmente di prendere come base questo suo progetto, da lui pubblicato sul proprio blog, perchè l'ho ritenuto il più interessante ed idoneo allo scopo cui mi prefiggevo. Ringrazio Luciano per questo ottimo esempio.
La soluzione è costituita da 5 progetti:
- il progetto principale FormsLoader
- la libreria comune CommonObjects
- 3 moduli 'satellite': ModuloProva, Modulo2 e Modulo3
La cosa interessante di questa tecnica è che un modulo viene gestito esattamente come un 'plugin', ovvero il programma principale non sa assolutamente nulla del modulo, e nemmeno deve essere informato in anticipo di quanti e quali moduli devono/possono essere utilizzati. Al contrario è il modulo che deve 'esporre' determinati attributi (ovviamente predisposti) che il programma principale sarà in grado di interpretare.
E' sufficiente infatti che il file del modulo (DLL) si trovi fisicamente in una cartella da noi predisposta che, nel nostro caso, si chiama \PLUGIN.
Naturalmente, come scritto sopra, la cosa più importante di un applicazione a moduli è che questi possano dialogare insieme.
Pensiamo ad esempio ad un modulo Fatture a cui sarà necessario comunicare l'ID del cliente, l'anno di esercizio, l'ID della azienda (nel caso in cui l'applicazione preveda la gestione multi-azienda, ecc.
In breve, una volta stabilite la modalità di gestione, potremo passare quanti parametri vogliamo. L'importante è che il modulo li sappia interpretare.
Al progetto iniziale ho inserito ulteriori caratteristiche come:
- La creazione di un menu a run-time e di un un toolstrip con tutti i collegamenti per aprire i vari form presenti nei moduli.
- Ho definito un overload al costruttore New() di ogni form che accetti i parametri da 'passare' tra applicazione principale e modulo secondario.
Non solo, come si vedrà nell'esempio, sarà anche possibile passare tali parametri tra moduli differenti. Vedi sotto.
- Ho aggiunto altri progetti, uno dei quali contiene un form MDI con cui aprire le proprie finestre child, passando i parametri anche a quest'ultime.
Ciò può consentire di creare complete applicazioni esterne che lavorano in sintonia con l'applicazione.
- I moduli possono 'dialogare' tra loro, ovvero, sarà possibile accedere da un modulo esterno ad un altro modulo esterno, direttamente, senza essere
costretti a passare per l'applicazione principale, anche qui passando gli opportuni parametri.
- Ho aggiunto ulteriori attributi alla classe ProgramsAttribute
-
Ho crearo un menu Finestra impostandolo come MdiWindowListItem, per elencare le finestre figlie.
Notare che tutte le finestre caricate dai moduli esterni, saranno correttamente elencate nel menu, ad esclusione della finestra del modulo MDI esterno, per ovvi motivi!
COME FUNZIONA
Il 'core' della gestione lo troviamo nel programma principale FormsLoader, nel form MainForm, in cui nell'evento Load viene eseguito il metodo CaricaAssembly per ogni modulo DLL trovato nella cartella \PLUGIN.
In ogni modulo vengono analizzati tutti i form, e quelli che sono stati 'decorati' con gli Attributi (definiti nella classe ProgramsAttribute del progetto CommonObjects), saranno ritenuti idonei e verrà creato un riferimento in modo da poterli avviare all'occorrenza.
Vediamo un esempio della gestione degli Attributi che 'decorano' le classi da caricare:
Nella classe ProgramsAttribute il costruttore New richiede 5 attributi:
Or AttributeTargets.Struct, AllowMultiple:=True)> _
Public Class ProgramsAttribute
Inherits Attribute
Private mGruppo As String
Private mModulo As String
Private mDescrizione As String
Private mNome As String
Private mAutore As String
Public Sub New(ByVal pGruppo As String, ByVal pModulo As String, ByVal pNome As String, ByVal pDescrizione As String, ByVal pAutore As String)
Me.Gruppo = pGruppo
Me.Modulo = pModulo
Me.Nome = pNome
Me.Descrizione = pDescrizione
Me.Autore = pAutore
End Sub
End Class
Nei form che voglio sia caricato all'avvio, devo assegnare il relativo valore ad ogni attributo:
Imports CommonObjects
Imports System.Windows.Forms
Public Class Form1
' implementazione di metodi, funzioni, ecc.
End Class
Se mancano gli attributi, il form non sarà caricato. Cosa vuol dire? Che io posso creare un modulo con un numero di form, ma caricarne 1 o 2, ed è proprio ciò che avviene ad esempio nel Modulo3 in cui carico solo il frmMain (MDI) ma non il frmChild, che invece sarà gestito all'interno del frmMain. Insomma, il modulo può essere proprio una applicazione completa!
Il MainForm.vb, al Load, andrà a caricare solo i form, di ogni modulo DLL, in cui avremo 'decorato' la classe con gli attributi richiesti, vediamo il pezzo di codice:
Dim atr() As Attribute = CType(t.GetCustomAttributes(GetType(ProgramsAttribute), True), Attribute())
If atr.Length <> 0 Then
'ricavo gli attributi per impostare i miei oggetti (menu, toolstrip, ecc.)
'che userò per avviare i singoli programmi.
End If
La parte 'finale' è quella che permette di avviare i programmi/form contenuti nei moduli, di cui riporto solo la parte cruciale, ovvero quella che serve ad aprire il form passando i parametri che desideriamo.
Ne ho definiti 2: NomeAzienda e IDAzienda.
Il metodo StartProgramFromMenuOrToolbar richiede
Private Sub StartProgramFromMenuOrToolbar(ByVal pObj As Object)
' ... codice ...
' converte il parametro pObj al tipo Form. Notare che il pObj non è altro che la proprietà
' Tag dell'Item (che abbiamo precedentemente valorizzato nel metodo CaricaAssembly)
Dim tipoForm As Type = DirectCast(pObj, Type)
' ... codice ...
' creo l'istanza, passando i parametri che ho definito
frm = DirectCast(Activator.CreateInstance(tipoForm, mAnno, mNomeAzienda, mIDAzienda), Form)
' ... codice ...
End Sub
Naturalmente, nel form chiamato si dovrà avere il relativo costruttore che accetta i parametri:
Public Class Form1
Dim mAnno As String
Dim mNomeAzienda As String
Dim mIDAzienda As Long
Private Sub New()
InitializeComponent()
End Sub
Public Sub New(ByVal pAnno As String, ByVal pNomeAzienda As String, ByVal pIDAzienda As Long)
InitializeComponent()
mAnno = pAnno
mNomeAzienda = pNomeAzienda
mIDAzienda = pIDAzienda
End Sub
' implementazione di metodi, funzioni, ecc.
End Class
VANTAGGI
- Riprendo quanto indicato da Luciano, sui vantaggi possibili di una tale implementazione:
- Avere sviluppatori diversi che si occupano ciascuno di una parte del progetto senza conflitti con gli altri progetti
- Possibilità di utilizzare per lo sviluppo di ciascun modulo uno qualsiasi dei linguaggi .NET.
- Gestire delle personalizzazioni in parole povere al posto della libreria standard utilizzare un’altra libreria modificando 'ad hoc' la libreria standard in tutto o in parte.
- Avere la possibilità di diversificare e specializzare l’applicativo (verticalizzazione), tenere riservate alcune librerie che vengono attivate solo in funzione di altri fattori
- Avere la possibilità di rendere modulare il proprio applicativo, nel senso che è possibile 'rilasciare' uno o più moduli in funzione del livello di licenza acquistata dal cliente.
- Aggiornare e distribuire solo le librerie che per un qualsiasi motivo devono essere aggiornate e/o modificate
- Realizzare una compilazione più veloce
- Gestire test e validazioni solo sugli oggetti modificati
Ora credo sia importante dalle parole passare ai fatti e provare la soluzione in questione,
fermo restando che essa vuole essere solo un esempio su cui ci sono ancora alcune cose da mettere a punto, ma mi sembra una buona base di partenza.
AVVERTENZA
Quando apportate modifiche ad un modulo satellite, ricordate che occorre compilare la DLL e copiarla
nella cartella dei PLUGIN, ovvero: ...\Modulare\FormsLoader\bin\Debug\plugin, altrimenti verrà usata sempre quella vecchia.