Per soddisfare la richiesta di qualche amico che mi ha chiesto delucidazioni in merito presento questa soluzione che esemplifica come ipoteticamente potrebbe essere realizzata la modularità nel caricamento delle forms su .NET.
Come è noto su .NET si possono utilizzare direttamente le librerie scritte in un linguaggio .NET.
Quest’ultima affermazione di solito si trova nelle prime pagine dei manuali, quelle che parlano del Framework .NET, pagine che di solito, grave errore di chi legge, vengono saltate.
Chiunque scrive una applicazione su .NET “sa” che è possibile introdurre nel proprio progetto il riferimento a librerie esterne create con uno qualsiasi degli altri linguaggi .NET, linguaggi in sostanza che producono tutti il medesimo codice “managed”.
Dopo l’inserimento dei riferimenti alla libreria, i tipi contenuti possono essere utilizzati direttamente dal progetto, ad esempio eventuali forms presenti nella libreria possono essere aperte allo stesso modo in cui sono attivate le forms del nostro progetto.
Esiste però un altro metodo di interfacciamento con le librerie esterne scritte per .NET, questo metodo prevede l’utilizzo della Reflection.
Troviamo quindi i due metodi di interfacciamento:
1) Inserimento della libreria tra i riferimenti con utilizzo diretto dei tipi
2) Caricamento dinamico della libreria utilizzando la Reflection
Il presente articolo riguarda il secondo metodo.
La tecnica di gestire dinamicamente il caricamento delle Dll consente di avere molteplici benefici come ad esempio:
- 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 tests e validazioni solo sugli oggetti modificati
Per la realizzazione si prevede di utilizzare un minimo di 3 progetti (2 principali + 1 "satellite").
- Progetto principale compilato come EXE WinForm, predisposto per gestire i moduli dell'applicazione e per gestire il caricamento dinamico degli stessi
- Progetto librerie comuni, compilato come DLL con lo scopo di mantenere una interfaccia comune tra i moduli
- Progetto libreria "plugin" contenente la parte specifica dell'applicativo (ad esempio, libreria vendite, libreria aquisti, libreria co.ge., ecc.)
La mia implementazione per l'esempio prevede pertanto 3 moduli:
- Progetto principale, "FormsLoader"
- Progetto libreria comune, denominato "CommonObjects"
- Progetto libreria di prova denominato "ModuloProva"
Il progetto “FormsLoader” prevede l’inserimento di “CommonObjects” tra i riferimenti del progetto.
Il progetto “CommonObjects” non ha bisogno di riferimenti esterni (se non quelli necessari per la compilazione del progetto).
Il progetto “ModuloProva” ha “CommonObjects” tra i riferimenti.
FormsLoader
Per poter realizzare il caricamento dinamico delle librerie plugin è necessario che il progetto principale individui la cartella da cui caricare le Dll plugin, sono utilizzabili tutti i percorsi validi come ad esempio "StartupPath" e/o impostazioni parametrizzate sui settaggi che facciano riferimento a posizioni assolute o relative.
La posizione della cartella plugin per FormsLoader è definita in una variabile di Settings che si chiama PluginLocation, su questa cartella devono confluire tutte le dll che si vogliono attivare.
Questa variabile viene data in pasto ad un metodo che enumera tutti i file presenti nella cartella definita come origine dei plugin.
For Each s As String In System.IO.Directory.GetFiles(sDir)
CaricaAssembly(s)
Next
Per ogni elemento incontrato nella cartella viene richiamato il metodo “CaricaAssembly” passandogli come argomento il nome completo della dll, in produzione è opportuno che si facciano maggiori controlli sulle dll da lanciare.
Per comprendere appieno i tests fatti nel caricamento è opportuno ricordare che su .NET esistono degli attributi con i quali si può “decorare” ogni classe e ogni metodo, per ciascuna classe / metodo sono possibili più attributi diversi.
Su CommonObjects ho definito appunto una classe che eredita da Attribute con la quale ho previsto di “decorare” le classi/forms da interfacciare nei plugins.
CaricaAssembly cicla sui tipi (Type) presenti sull’assembly, la funzione GetTypes() restituisce tutti i tipi contenuti.
Ogni form (oggetto di tipo form) viene inserita come riferimento nel “Tag” di un oggetto di tipo ListViewItem, ListViewItem che a sua volta alimenta una ListView per la presentazione nel menù utente.
Naturalmente sono possibili a questo livello le più ampie impostazioni e scelte, sia stilistiche che tecniche, solo a titolo di esempio una treeview con una distribuzione “ad albero” delle varie gerarchie di moduli e una listview con tutti i figli contenuti nella treeview su cui si è posizionati.
Private Sub CaricaAssembly(ByVal pAssemblyName As String)
If System.IO.File.Exists(pAssemblyName) Then
Try
Dim asmb As Assembly = Assembly.LoadFrom(pAssemblyName)
For Each t As Type In asmb.GetTypes()
If t.BaseType.Equals(GetType(Form)) Then
Dim atr() As Attribute = t.GetCustomAttributes(GetType(ProgramsAttribute), True)
If atr.Length <> 0 Then
Dim pgm As ProgramsAttribute = CType(atr(0), ProgramsAttribute)
Dim li As New ListViewItem
IndexBitmap += 1
li.Tag = t
li.Text = pgm.Descrizione
li.ImageIndex = CType(IndexBitmap Mod ImagePgm.Images.Count, Integer)
ListaPgm.Items.Add(li)
End If
End If
Next
Catch ex As Exception
MessageBox.Show("Impossibile caricare " + pAssemblyName + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace)
End Try
End If
End Sub
Ho impostato un evento "DoubleClick" (doppio click) sul ListView presente nella form principale (MainForm) sul gestore dell'evento tramite un semplice controllo è definita e richiamata la form memorizzata sull'elemento della ListView.
Private Sub StartProgram(ByVal pObj As Object, ByVal pItem As ListViewItem)
Dim found As Boolean = False
Dim tipoForm As Type = DirectCast(pObj, Type)
Dim frm As Form = Nothing
For Each f As Form In Me.OwnedForms
If f.[GetType]().Equals(tipoForm) Then
found = True
frm = f
Exit For
End If
Next
If Not found Then
Dim img As Image = DirectCast(ImagePgm.Images(pItem.ImageIndex), Image)
frm = DirectCast(Activator.CreateInstance(tipoForm), Form)
Dim oBitmap As Bitmap = DirectCast(ImagePgm.Images(pItem.ImageIndex), Bitmap)
frm.Icon = System.Drawing.Icon.FromHandle(oBitmap.GetHicon())
oBitmap.Dispose()
frm.Owner = Me
Else
frm.Activate()
frm.WindowState = FormWindowState.Normal
End If
frm.Show()
End Sub
CommonObjects
Per ogni form collegata durante il caricamento dell'assembly è previsto siano definiti alcuni parametri impostati tramite l'utilizzo di una classe apposita che eredita da Attribute che definisce 3 campi: Gruppo, Modulo e Descrizione.
Gruppo e Modulo possono essere utilizzati come indicatori del livello su cui appendere la form indirizzata, ad esempio se si genera un menu con sottomenu oppure una treeview con nodi padre e figlio.
Or AttributeTargets.Struct, AllowMultiple:=True)> _
Public Class ProgramsAttribute
Inherits Attribute
Private mGruppo As String
Private mModulo As String
Private mDescrizione As String
Public Sub New(ByVal pGruppo As String, ByVal pModulo As String, ByVal pDescrizione As String)
Me.Gruppo = pGruppo
Me.Descrizione = pDescrizione
Me.Modulo = pModulo
End Sub
Private Sub New()
End Sub
Public Property Gruppo() As String
Get
Return mGruppo
End Get
Set(ByVal Value As String)
mGruppo = Value
End Set
End Property
Public Property Modulo() As String
Get
Return mModulo
End Get
Set(ByVal Value As String)
mModulo = Value
End Set
End Property
Public Property Descrizione() As String
Get
Return mDescrizione
End Get
Set(ByVal Value As String)
mDescrizione = Value
End Set
End Property
End Class
Questo modulo "ProgramsAttribute" è presente in un progetto DLL (CommonObjects) che è richiamato anche dalle forms "satelliti", ogni form (o meglio la classe form) è "decorata" con gli attributi
"Gruppo1", "Prima Form", "Descrizione Prima Form")> _
secondo la normale sintassi relativa agli attributi personalizzati.
ModuloProva
Il terzo progetto: "ModuloProva" è un progetto DLL contiene le forms, naturalmente è possibile definire più progetti e più dll per le forms.
"Gruppo1", "Prima Form", "Descrizione Prima Form")> _
Public Class PrimaForm
End Class
Il risultato è che un progetto che evidenzia come definire e implementare il caricamento dinamico delle forms.