I Delegates e il loro uso al di fuori della gestione eventi - seconda parte
a cura di Sabrina Cosolo (requisiti: voglia di impazzire)Nella prima parte, abbiamo creato il progetto e sviluppato le classi MenuItem, MenuItems e Menu.
In questa seconda parte svilupperemo le classi che ci permetteranno di realizzare il nocciolo della nostra applicazione, ovvero la Collezione di Delegate ove memorizzare i puntatori ai metodi della nostra applicazione e la gestione della esecuzione dei metodi in base al nostro menu parametrico.
Per realizzare questa parte del progetto, utilizzeremo anche in questo caso tre classi concentriche. La classe MethodsManager, che fornirà la lista dei metodi eseguibili e il metodo per eseguirli, la classe MenuMethods, che fornirà la struttura della collezione di metodi, e la classe MenuMethod, che fornirà il singolo elemento della nostra collezione di metodi.
Struttura delle classi
- MenuMethod
questa classe espone 3 proprietà ed un metodo:
- Key - Chiave (nome) del metodo
- Method - Delegate del metodo
- MethodType - Tipo del metodo delegato
- ToString - metodo che visualizza il contenuto della classe come stringa, (restituisce la Key).
- MenuMethods
Questa classe eredita dalla classe Generic List ed espone una proprietà ed un metodo
- Item - Indexer secondario: permette di ricavare un elemento tramite la sua Key
- ToString - metodo che restituisce una lista delle Key degli elementi della collezione.
- MethodsManager
Questa classe fornisce una proprietà, un Delegate e un metodo
- MethodList - Collezione ove memorizzare i delegate ai metodi da utilizzare per il menu parametrico.
- MenuMethodHandlerVoid - Delegate tipizzato per produrre i delegate ai metodi da inserire nel menu parametrico.
- ExecuteMethod - Metodo per l'esecuzione di uno dei metodi memorizzati nella collezione.
MenuMethod
Come la classe MenuItem fornisce la struttura per la singola opzione di Menu, MenuMethod fornisce la struttura ove memorizzare il puntatore al metodo relativo all'opzione di menu, il legame fra i due è dato dalla proprietà Key che, inserita nel Menu, permetterà al programma di selezionare correttamente il metodo da chiamare.
Al contrario della classe MenuItem, questa classe non è serializzabile: infatti, essa esiste esclusivamente nel momento in cui il programma viene instanziato e muore, assieme al suo contenuto, nel momento in cui il programma termina. Non è infatti utile persistere dei puntatori a metodo.Per generare la classe, facciamo click con il tasto destro sul progetto nel solution explorer e selezioniamo Add -> Class, chiamiamo la classe MenuMethod e diamo OK.
Namespaces:
sono simili a quelli delle classi per i menu, abbiamo solo tolto il namespace per la serializzazione.
VBImports System Imports System.Text
C#using System; using System.Text; namespace VbTips { .... }La Dichiarazione della classe:
come per la classe MenuItem, non ci sono particolari predisposizioni o interfacce
VBPublic Class MenuMethod .... End Class
C#public class MenuMethod { .... }I Campi privati della classe:
Oltre al solito mClassName per la gestione delle eccezioni, ci sono mKey, una chiave univoca, che corrisponde a quella dell'opzione di menu, e mMethod, il Delegate che memorizza il puntatore al metodo.
VBPrivate mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name Private mKey As String Private mMethod As System.Delegate
C#private readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; private string mKey; private Delegate mMethod;Il Costruttore:
Il costruttore, a differenza di quello delle classi finora definite, è fornito di due parametri obbligatori, che (guarda caso) sono i dati fondamentali che questa classe fornisce ad un programma, ovvero la Chiave con cui inserire il metodo a Menu e il Delegate al metodo stesso. Questi due dati non potranno essere modificati durante la vita della classe, potranno essere ridefiniti solo rilasciando l'instanza e instanziandone una nuova.
VBPublic Sub New(ByVal pKey As String, ByVal pMethod As System.Delegate) Me.mKey = pKey Me.mMethod = pMethod End Sub
C#public MenuMethod(string pKey, Delegate pFunction) { this.mKey = pKey; this.mMethod = pFunction; }La definizione delle proprietà:
VBPublic ReadOnly Property Key() As String Get Return (Me.mKey) End Get End Property Public ReadOnly Property Method() As System.Delegate Get Return Me.mMethod End Get End Property
C#public string Key { get { return mKey; } } public Delegate Method { get { return (mMethod); } }Esponiamo il contenuto dei campi della classe e, come possiamo notare, abbiamo definito proprietà a sola lettura, in quanto i campi possono essere inizializzati solo al momento della generazione della classe tramite i parametri del costruttore. Non c'è un motivo preciso per farlo, tranne che, quando in un programma definiamo un Delegate ad un metodo, non è a mio avviso una bella cosa poterlo modificare e ridefinire a piacere, per cui, il solo scopo di questo tipo di definizione è quello di obbligarci a ridefinire l'oggetto MenuMethod se lo vogliamo modificare.
Il Metodo ToString:
In questo caso, ToString ci fornisce solo il nome del Metodo, e potrebbe tornarci utile per visualizzare una lista dei metodi inseriti in una collezione, quella che andremo a definire fra poco.
VBPublic Overrides Function ToString() As String Return Me.Key End Function
C#public override string ToString() { return this.Key; }MenuMethods
Come MenuItems formisce una collection di MenuItem, MenuMethods fornisce una collection di MenuMethod, anche questa collection, come gli elementi che contiene, non implementa la serializzazione, però in questo caso, implementeremo un nuovo oggetto, o meglio una nuova property che fornirà un indexer che ci permetterà di accedere agli elementi della collezione per Key.Per generare la classe, click con il tasto destro sul progetto nel solution explorer e selezioniamo Add -> Class chiamiamo la classe MenuMethods e diamo OK.
Namespaces:
Anche in questo caso nulla di nuovo, solo quanto già utilizzato in precedenza.
VBImports System Imports System.Collections.Generic Imports System.Text Imports System.Collections
C#using System; using System.Collections.Generic; using System.Collections; using System.Text; namespace VbTips { ... }La Dichiarazione della classe:
Anche questa classe eredita dalla classe Generic List, che ci fornisce quasi tutto quello che ci serve per il nostro uso.
VBPublic Class MenuMethods Inherits List(Of MenuMethod) .... End Class
C#public class MenuMethods : List<MenuMethod> { .... }I Campi privati della classe:
Non ci sono campi privati, salvo il nostro onnipresente campo Nome della classe.
VBPrivate mClassName As String _ = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name
C#private readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name;Il Costruttore:
Non c'è alcun costruttore in quanto non c'è nulla da inizializzare, pertanto ci basta il costruttore standard della classe base.La definizione delle proprietà:
VBPublic Overloads Property Item(ByVal pKey As String) As MenuMethod Get Try Dim retItem As MenuMethod = Nothing For i As Integer = 0 To Me.Count - 1 If (Me(i).Key = pKey) Then retItem = Me(i) Exit For End If Next Return (retItem) Catch ex As Exception Throw New ApplicationException(" " _ & Me.mClassName & "." _ & System.Reflection.MethodBase.GetCurrentMethod().Name _ & ": " & ex.Message, ex) End Try End Get Set(ByVal value As MenuMethod) Try Dim itemFound As Boolean = False For i As Integer = 0 To Me.Count - 1 If (Me(i).Key = pKey) Then Me(i) = value itemFound = True Exit For End If Next If (Not itemFound) Then Throw New _ ApplicationException(String.Format(My.Resources.warItemNotFound, pKey)) End If Catch ex As Exception Throw New ApplicationException(" " _ & Me.mClassName & "." _ & System.Reflection.MethodBase.GetCurrentMethod().Name _ & ": " & ex.Message, ex) End Try End Set End Property
C#public MenuMethod this[string pKey] { get { try { MenuMethod retItem = null; for (int i = 0; i < this.Count; i++) { if (this[i].Key == pKey) { retItem = this[i]; break; } } return (retItem); } catch (Exception ex) { throw new ApplicationException(" " + this.mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name + ": " + ex.Message, ex); } } set { try { bool itemFound = false; for (int i = 0; i < this.Count; i++) { if (this[i].Key == pKey) { this[i] = value; itemFound = true; break; } } if (!itemFound) { throw new ApplicationException( string.Format(Properties.Resources.warItemNotFound, pKey)); } } catch (Exception ex) { throw new ApplicationException(" " + this.mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name + ": " + ex.Message, ex); } } }Il solo punto diverso dalla precedente collezione sviluppata è questo, ovvero la definizione di una property Indexer secondaria che ci permetta di trovare un elemento della collezione tramite la sua property Key. L'algoritmo di ricerca utilizzato è banale, ma nel nostro esempio la collezione contiene pochi elementi, volendo implementare un simile tipo di ricerca in collezioni estese (ad esempio una classe che contenga una tabella di un database) potremmo utilizzare dei meccanismi di ricerca più sofisticati, come ad esempio quelli forniti dalla classe BindingList.
Il Metodo ToString:
La funzione ToString semplicemente lista i metodi contenuti per Key usando la funzione ToString degli elementi contenuti.
Anche in questo caso, non è prevista serializzazione.
VBPublic Overrides Function ToString() As String Try Dim sb As StringBuilder = New StringBuilder(String.Empty) For Each item As MenuMethod In Me sb.AppendLine(item.ToString()) Next Return (sb.ToString) Catch ex As Exception Throw New ApplicationException(" " _ & Me.mClassName & "." _ & System.Reflection.MethodBase.GetCurrentMethod().Name _ & ": " & ex.Message, ex) End Try End Function
C#public override string ToString() { try { StringBuilder sb = new StringBuilder(string.Empty); foreach (MenuMethod item in this) { sb.AppendLine(item.ToString()); } return (sb.ToString()); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name + ": " + ex.Message, ex); } }MethodsManager
Questa classe, che fa da contenitore alla collezione di metodi, fornisce inoltre alcuni dati accessori e soprattutto il metodo per eseguire le funzioni Delegate.Per generare la classe, click con il tasto destro sul progetto nel solution explorer e selezioniamo Add -> Class, chiamiamo la classe MethodsManager e diamo OK
Namespaces:
Anche per questa classe nulla di nuovo nei namespaces
VBImports System Imports System.Collections.Generic Imports System.Text
C#using System; using System.Collections.Generic; using System.Text; namespace VbTips { ... }La Dichiarazione della classe:
Ed anche la classe non ha nulla di diverso dalle classi base precedenti.
VBPublic Class MethodsManager .... End Class
C#public class MethodsManager { .... }I campi privati della classe e il delegate pubblico:
VBPrivate mClassName As String _ = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name Private mMethodList As MenuMethods Public Delegate Sub MenuMethodHandlerVoid()
C#private readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; private MenuMethods mMethodList; public delegate void MenuMethodHandlerVoid();Qui invece abbiamo una differenza, oltre alla solita variabile con il nome della classe, e alla dichiarazione della collezione di Metodi che il MethodManager gestisce, abbiamo un terzo oggetto, ovvero la definizione del Delegate pubblico che farà da stampino a tutti i metodi che inseriremo all'interno della nostra classe. Questo Delegate dichiara semplicemente che i metodi delegati che inseriremo nella collezione sono void senza parametri (Sub senza parametri).
Il Costruttore:
Nel costruttore, inizializziamo la collezione di metodi.
VBPublic Sub New() Me.mMethodList = New MenuMethods() End Sub
C#public MethodsManager() { this.mMethodList = new MenuMethods(); }La definizione delle proprietà:
Esponiamo la nostra collezione di metodi affinché possa essere riempita.
VBPublic Property MethodList() As MenuMethods Get Return mMethodList End Get Set(ByVal value As MenuMethods) mMethodList = value End Set End Property
C#public MenuMethods MethodList { get { return mMethodList; } set { mMethodList = value; } }Il Metodo ToString:
Se richiesto, visualizziamo la lista dei metodi inseriti.
VBPublic Overrides Function ToString() As String Return Me.MethodList.ToString() End Function
C#public override string ToString() { return (this.MethodList.ToString()); }Il Metodo ExecuteMethod:
VBPublic Sub ExecuteMethod(ByVal pKey As String) Try Dim meto As MenuMethod = Me.MethodList.Item(pKey) If (Not meto Is Nothing) Then meto.Method.DynamicInvoke(Nothing) Else Throw New ApplicationException(My.Resources.warFnzTodo) End If Catch ex As Exception Throw New ApplicationException(" " _ & Me.mClassName & "." _ & System.Reflection.MethodBase.GetCurrentMethod().Name _ & ": " & ex.Message, ex) End Try End Sub
C#public void ExecuteMethod(string pKey) { try { MenuMethod meto = this.MethodList[pKey]; if (meto != null) { meto.Method.DynamicInvoke(null); } else { throw new ApplicationException(Properties.Resources.warFnzTodo); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name + ": " + ex.Message, ex); } }E questo è il solo oggetto diverso dagli altri, il cuore di questa classe, ovvero un metodo che esegue i metodi inseriti nella nostra collezione su richiesta.
Per fare ciò, utilizza un metodo della classe Delegate, che si chiama DynamicInvoke, a cui è possibile passare un array di oggetti che rappresentano i parametri delle funzioni delegate, quindi nel nostro caso passiamo un null o Nothing visto che non ci sono parametri. Nel caso venga richiesta l'esecuzione di un metodo inesistente, lanciamo un'eccezione che indica che il metodo è ancora da sviluppare.Ed ora, direi che è arrivato il momento di provare le classi che abbiamo sviluppato, ma prima vi devo chiarire una incongruenza che forse avrete notato nella prima parte: al momento della creazione del nostro progetto Winforms, è stato creato il Form1, ma nel codice della Sub Main che vi ho mostrato c'era già il nome FrmMain. Questo è successo perché questo cambiamento di nome è per me la prima e più importante operazione da fare e la faccio talmente d'abitudine da dimenticarmi di spiegarvela.
Quindi, se seguite il progetto assieme a me, dovete riprendere in mano il Form1 e rinominarlo FrmMain utilizzando il Solution Explorer per rinominare il file Form1.vb (o Form1.cs). Se il nostro ambiente di sviluppo è configurato correttamente, provando ad entrare in FrmMain in vista codice, troveremo che la classe è stata rinominata e, inoltre, che le funzioni di refactoring hanno rinominato la classe anche nel Main che si trova in Program.cs o Program.vb. Se così non fosse, provvediamo manualmente.
Avviamo l'applicazione e vediamo se la form appare.
FrmMain
La form principale nei miei programmi solitamente si chiama sempre così, ed è la prima form ad essere aperta e l'ultima ad essere chiusa.Chi facesse il progetto VB, si accorgerà che nella visualizzazione codice della form, quando viene generata, compare solo la definizione della classe, non contiene il costruttore, non contiene le clausole di importazione dei Namespaces, ebbene, questo è dovuto esclusivamente a come è disegnato il template del progetto e delle form in Visual Basic.
I template possono essere modificati e arricchiti in base alle nostre esigenze, siano essi in C# o in VB, magari in un altro articolo, vedremo di spiegare come si fa.Una cosa che noterete se osservate il progetto C# rispetto a quello VB è l'uso delle #region #endregion che è applicabile allo stesso modo in VB (ma che nei template C# ho messo in automatico e invece per pigrizia in VB non ho aggiunto). Dividere le classi in Regioni (a volte anche a più livelli) è utile, perché la formattazione prodotta dall'editor di Visual Studio rende più semplice trovare le cose in fase di manutenzione di codice scritto magari anni prima.
Un'altra cosa che ho omesso nell'articolo ma non all'interno dei progetti sono i commenti XML, il metodo più semplice per far sì che tutto ciò che sviluppate venga recepito da intellisense come accade con le classi Microsoft, ed il modo più semplice per ottenere una documentazione HTML automatica e gratuita delle vostre librerie di classi (cercate NDoc su Internet). Per generare questi commenti, oltre agli automatismi forniti dall'editor VB ci sono alcuni strumenti utili che si possono installare come plugin di Visual Studio e producono delle cose molto utili.
Namespaces:
VBImports System Imports System.Collections.Generic Imports System.ComponentModel Imports System.Data Imports System.Drawing Imports System.Text Imports System.Windows.Forms Imports System.Net
C##region Using directives using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Net; #endregion namespace VbTips { ... }Come vedete abbiamo messo qualche namespace in più, alcuni collegati ai windows forms, altri ad alcune delle cose che svilupperemo.
La Dichiarazione della classe:
VBPublic Class FrmMain Public Sub New() InitializeComponent() End Sub End Class
C#public partial class FrmMain : Form { #region Costruttore public FrmMain() { InitializeComponent(); } #endregion }La differenza primaria fra le due classi è la mancanza della clausola Inherits sotto a FrmMain e la clausola partial prima di Class. Si tratta solo di uno degli stratagemmi adottati dal team di sviluppo di Microsoft per limitare le paure dei programmatori provenienti dal VB6: se aprite la FrmMain.Designer.vb, troverete che la clausola Inherits è stata inserita in quella porzione della classe assieme alla dicitura Partial.
Modifica leggermente polemica (ma istruttiva)
A questo punto, prima di continuare il nostro progetto - a causa del mio caro amico Alberto De Luca, 'talebano' del VB :o), il quale mi ha lanciato un anatema, un lunedì mattina, dicendomi che sono un'eretica, perché ho eliminato l'Application Framework e quindi non posso più godere dei benefici da esso forniti, quali gli eventi che ci indicano che la rete è caduta e simili - faccio una minuscola modifica alla form, per dimostrargli che il Framework è sempre li, anche se non utilizziamo le famigerate classi wrapper che Microsoft ha introdotto per semplificare la vita ai principianti.Se osservate la lista dei Namespaces che ho inserito, potete vedere che fra i namespaces c'è System.Net. Guarda caso, in questo namespace c'è tutto quanto riguarda la gestione di una rete.
Ed ecco la modifica:
VBPublic Sub New() InitializeComponent() AddHandler _ System.Net.NetworkInformation.NetworkChange.NetworkAvailabilityChanged _ , AddressOf NetworkChange_NetworkAvailabilityChanged InitializeMethodsManager() End Sub Private Sub NetworkChange_NetworkAvailabilityChanged( _ ByVal sender As Object _ , ByVal e As System.Net.NetworkInformation.NetworkAvailabilityEventArgs) If (Not e.IsAvailable) Then MessageBox.Show("La rete non è più disponibile") Else MessageBox.Show("La rete è nuovamente disponibile") End If End Sub
C##region Costruttore public FrmMain() { InitializeComponent(); System.Net.NetworkInformation.NetworkChange.NetworkAvailabilityChanged += new System.Net.NetworkInformation.NetworkAvailabilityChangedEventHandler (NetworkChange_NetworkAvailabilityChanged); InitializeMethodsManager(); } #endregion void NetworkChange_NetworkAvailabilityChanged(object sender , System.Net.NetworkInformation.NetworkAvailabilityEventArgs e) { if (e.IsAvailable) { MessageBox.Show("La rete è nuovamente disponibile"); } else { MessageBox.Show("Non c'è più rete"); } }Abbiamo aggiunto un Handler all'evento NetworkAvailabilityChanged ed iniziato così ad usare i Delegate, creando un delegate alla funzione NetworkChange_NetworkAvailabilityChanged, che è stato assegnato all'evento.
Se il vostro pc è collegato in rete ad una Lan oppure ad un Router Internet, provate ora a lanciare il programma e staccare il cavo di rete (o a spegnere la wireless); dopo qualche secondo, oltre al popup di Windows XP che vi dice che la rete è sconnessa, apparirà anche la messagebox che ci dice che la rete non è più disponibile all'interno del nostro progetto; riagganciamola e, anche in questo caso, dopo qualche secondo ci viene comunicato che la rete è stata riconnessa.
Questo tipo di funzionalità potrà arricchire in modo notevole i nostri programmi Winforms, permettendoci di capire se è successo qualcosa alla connessione di rete e magari evitando a chi sta modificando dati che il programma si inchiodi perdendo tutte le modifiche effettuate.Oltre alla gestione della disponibilità di rete, nel costruttore abbiamo inserito anche la chiamata ad una funzione InitializeMethodsManager che predisporrà la collezione dei delegate a funzione che il nostro progetto ci rende disponibili, ma la discuteremo al momento opportuno, più avanti in questo articolo.
mnuMain.xml
Terminata la disquisizione estemporanea sugli eventi di sistema, torniamo a noi, provando a rendere interessante questo piccolo progetto con alcune modifiche a quella che doveva essere la struttura dimostrativa iniziale.Nell'editor di Visual Studio, generiamo un nuovo file XML:
click con il tasto destro sul progetto, Add -> New Item -> XML File e chiamiamo il file mnuMain.xml.<?xml version="1.0" encoding="utf-8" ?> <Menu xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.visual-basic.it"> <Key>mnuMain</Key> <Title>Menu Principale</Title> <Items> <MenuItem> <Description>&File</Description> <Key>mnuFile</Key> </MenuItem> <MenuItem> <Description>&Test</Description> <Key>mnuTest</Key> </MenuItem> </Items> </Menu>Come potete notare, ho fatto alcune modifiche rispetto al file originale che avevo predisposto all'inizio di questo articolo. Questo perché invece di un solo file XML per il test ne faremo tre.
Torniamo sul progetto, tasto destro, Add -> New Item -> XML File, mnuFile.xml
mnuFile.xml
<?xml version="1.0" encoding="utf-8" ?> <Menu xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.visual-basic.it"> <Key>mnuFile</Key> <Title>Menu File</Title> <Items> <MenuItem> <Description>&Impostazioni di Stampa</Description> <Key>fnzImpostaStampa</Key> </MenuItem> <MenuItem> <Description>&Exit</Description> <Key>exit</Key> </MenuItem> </Items> </Menu>Non accontentiamoci di due soli menu, aggiungiamo anche il terzo...
Solution explorer, progetto, click destro, Add -> New Item -> XML File, mnuTest.xml.
mnuTest.Xml
<?xml version="1.0" encoding="utf-8"?> <Menu xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.visual-basic.it"> <Key>mnuTest</Key> <Title>Menu Test</Title> <Items> <MenuItem> <Description>Metodo &Uno</Description> <Key>fnzUno</Key> </MenuItem> <MenuItem> <Description>Metodo &Due</Description> <Key>fnzDue</Key> </MenuItem> </Items> </Menu>Una volta compilati i tre files, copiamoli sulla cartella c:\ che useremo per i nostri test, oppure, se preferite usarne una diversa, ricordatevi di cambiare il path dei files ove lo imposteremo.
Visto e considerato che abbiamo scritto tutto questo XML, vediamo un poco di capire che cosa abbiamo costruito: si tratta di una struttura di menu, mnuMain è il menu principale, che contiene due sottomenu, mnuFile e mnuTest; ciascuno di questi due menu, a sua volta, contiene due opzioni: mnuFile ha un'opzione imposta stampante ed un'opzione per uscire dal programma, mnuTest contiene due metodi che invocano qualcosa, fnzUno e fnzDue.
Ho deciso di adottare alcune convenzioni relative ai parametri Key delle opzioni di menu, e sono le seguenti:
- Se un opzione dà accesso ad un sottomenu, si chiama mnuNomeMenu
- Se un opzione dà accesso ad un metodo si chiama fnzNomeFunzione
- L'opzione exit è per convenzione l'uscita da programma.
Di nuovo FrmMain
Andiamo ora a modificare la nostra FrmMain per inserirvi il necessario alle nostre prove:
- Modifichiamo per prima cosa la property Text della form facendola diventare: Test VbTips Delegates C# (VB)
- Poniamo a True il flag IsMdiContainer trasformando questa form in una form MDI.
- Dalla Toolbox di Visual Studio 2005 trasciniamo nella form un controllo MenuStrip.
- Cambiamo il suo nome in mnuMain.
- Facciamo doppio click sulla form per accedere al codice dell'evento Load della stessa.
Variabili private:
Nelle variabili private, predisponiamo la stringa che ci permetterà di comporre il path corretto dei menu che andremo a leggere, se non usiamo c:\ è qui che possiamo modificare il path inserendo quello che abbiamo deciso di usare.
VBPrivate ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name Private ReadOnly mMenuBasePath As String = "c:\{0}.xml"
C#private readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; private readonly string mMenuBasePath = "c:\\{0}.xml";FrmMain Load Event handler:
VBPrivate Sub FrmMain_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Try Dim mnu As VbTips.Menu = VbTips.Menu.ReadXml("c:\mnuMain.xml", False) Me.mnuMain.Items.Clear() Dim mnuMain As VbTips.Menu = VbTips.Menu.ReadXml( _ String.Format(Me.mMenuBasePath, "mnuMain"), False) For Each mnuOption As MenuItem In mnu.Items Dim item As ToolStripMenuItem = New ToolStripMenuItem(mnuOption.Description) item.Name = mnuOption.Key Me.mnuMain.Items.Add(item) If (mnuOption.Key.StartsWith("mnu")) Then Dim subMenu As VbTips.Menu = VbTips.Menu.ReadXml(_ String.Format(Me.mMenuBasePath, mnuOption.Key), False) For Each subOption As MenuItem In subMenu.Items Dim subItem As ToolStripMenuItem = _ New ToolStripMenuItem(subOption.Description) subItem.Name = subOption.Key AddHandler subItem.Click, AddressOf Me.MenuItem_Click item.DropDownItems.Add(subItem) Next End If Next Catch ex As Exception MessageBox.Show(ex.Message, "ERRORE" _ , MessageBoxButtons.OK, MessageBoxIcon.Error) End Try End Sub
C#private void FrmMain_Load(object sender, EventArgs e) { try { this.mnuMain.Items.Clear(); VbTips.Menu mnuMain = VbTips.Menu.ReadXml( string.Format(this.mMenuBasePath,"mnuMain"),false); foreach( MenuItem mnuOption in mnuMain.Items ) { ToolStripMenuItem item = new ToolStripMenuItem(mnuOption.Description); item.Name = mnuOption.Key; this.mnuMain.Items.Add(item); if (mnuOption.Key.StartsWith("mnu")) { VbTips.Menu subMenu = VbTips.Menu.ReadXml( string.Format(this.mMenuBasePath, mnuOption.Key), false); foreach (MenuItem subOption in subMenu.Items) { ToolStripMenuItem subItem = new ToolStripMenuItem(subOption.Description); subItem.Name = subOption.Key; subItem.Click += new EventHandler(MenuItem_Click); item.DropDownItems.Add(subItem); } } else { item.Click += new EventHandler(MenuItem_Click); } } } catch (Exception ex) { MessageBox.Show(ex.Message, "ERRORE" , MessageBoxButtons.OK, MessageBoxIcon.Error); } }Una nota prima di descrivere quel che abbiamo fatto:
Esiste una classe Menu anche nel namespace standard di Visual Studio, pertanto, la nostra classe Menu viene definita con il Namespace completo, così da far capire al compilatore di che classe si tratta, questa è una piccola dimostrazione dell'utilità e dell'uso dei Namespaces.Vediamo un po' che cosa fa il nostro metodo di caricamento:
- Cancella il contenuto del MenuStrip mnuMain.
- Genera un oggetto VbTips.Menu deserializzando il file XML mnuMain.xml da noi predisposto allo scopo, e, come potete vedere, per deserializzare l'oggetto chiamiamo un metodo statico (shared) senza dover per forza instanziare un oggetto visto che il metodo stesso ce ne genera uno.
- Cicla la collezione degli Items del menu appena generato e genera un oggetto TabStripMenuItem per ognuno di essi, inserendo nel nome dell'oggetto la Key del nostro MenuItem e nella proprietà Text la Description del nostro MenuItem. Inoltre, visto che abbiamo voluto fare un test strutturato, se la Key del MenuItem inizia per mnu, viene deserializzato il Sottomenu relativo e vengono generate all'interno del TabStripMenuItem, le opzioni contenute nel sottomenu.
Essendo questo un test, ci siamo limitati a scendere di un solo livello, nulla vieta che volendo possiamo predisporre una funzione ricorsiva e quindi generare una struttura menu complessa.- Per terminare la preparazione del menu, aggiungiamo un handler alla funzione di gestione del Click su ognuno degli Item che non sono a loro volta dei Menu.
- Faccio notare che in questo caso, la gestione eccezioni (Try Catch), visto che ci troviamo al più alto livello del programma, ovvero l'interfaccia utente, invece di rilanciare l'eccezione, semplicemente la visualizza all'utente in una messagebox. Ovviamente, questo, in un progetto reale, è il punto ove inserire la funzione che identifica l'eccezione e la gestisce indicando all'utente quali azioni compiere se necessarie.
Se avviamo la nostra applicazione, otteniamo una Form MDI, con un Menu principale, con due voci File e Test, ciascuna con due opzioni.
La funzione InitializeMethodsManager e le funzioni di test:
Torniamo alla funzione InitializeMethodsManager, che avevamo abbandonato nel costruttore della classe.
Questa funzione ci permette di generare la collezione dei metodi che possono essere eseguiti dai nostri menu parametrici; qui generiamo il MethodsManager e riempiamo la sua collezione di metodi con i delegate di tre funzioni di test, che al momento eseguono unicamente una messagebox.
VBPrivate Sub InitializeMethodsManager() mMetoMgr = New MethodsManager() Dim meto As MenuMethod = _ New MenuMethod("fnzUno", _ New MethodsManager.MenuMethodHandlerVoid( _ AddressOf TestFnzUno)) Me.mMetoMgr.MethodList.Add(meto) meto = _ New MenuMethod("fnzDue", _ New MethodsManager.MenuMethodHandlerVoid( _ AddressOf TestFnzDue)) Me.mMetoMgr.MethodList.Add(meto) meto = _ New MenuMethod("fnzImpostaStampa", _ New MethodsManager.MenuMethodHandlerVoid( _ AddressOf TestFnzImpostaStampa)) Me.mMetoMgr.MethodList.Add(meto) End Sub Private Sub TestFnzUno() MessageBox.Show("Funzione di test numero uno") End Sub Private Sub TestFnzDue() MessageBox.Show("Funzione di test numero due") End Sub Private Sub TestFnzImpostaStampa() MessageBox.Show("Funzione di impostazione pagina di stampa") End Sub
C#private void InitializeMethodsManager() { this.mMetoMgr = new MethodsManager(); MenuMethod meto = new MenuMethod("fnzUno", new VbTips.MethodsManager.MenuMethodHandlerVoid(TestFnzUno)); mMetoMgr.MethodList.Add(meto); meto = new MenuMethod("fnzDue", new VbTips.MethodsManager.MenuMethodHandlerVoid(TestFnzDue)); mMetoMgr.MethodList.Add(meto); meto = new MenuMethod("fnzImpostaStampa", new VbTips.MethodsManager.MenuMethodHandlerVoid(TestFnzImpostaStampa)); mMetoMgr.MethodList.Add(meto); } public void TestFnzUno() { MessageBox.Show("Ho eseguito la funzione di test UNO"); } public void TestFnzDue() { MessageBox.Show("Ho eseguito la funzione di test DUE"); } public void TestFnzImpostaStampa() { MessageBox.Show("Funzione di impostazione pagina di stampa"); }L'Event handler dell'evento click sulle opzioni di menu:
VBPrivate Sub MenuItem_Click(ByVal sender As Object, ByVal e As EventArgs) Dim item As ToolStripMenuItem = CType(sender, ToolStripMenuItem) If (Not item.Name.StartsWith("mnu")) Then If (item.Name = "exit") Then Application.Exit() Else mMetoMgr.ExecuteMethod(item.Name) End If End If End Sub
C#void MenuItem_Click(object sender, EventArgs e) { ToolStripMenuItem item = sender as ToolStripMenuItem; if (!item.Name.StartsWith("mnu")) { if (item.Name == "exit") { Application.Exit(); } else { this.mMetoMgr.ExecuteMethod(item.Name); } } }Questo evento si occupa della gestione del nostro menu parametrico.
In base alle convenzioni, scarta tutte le opzioni che fossero dei Menu, anche se, per come abbiamo strutturato la generazione del menu stesso, queste opzioni non scateneranno alcun evento. Se l'opzione si chiama Exit, viene chiamata la chiusura dell'applicazione, altrimenti viene eseguita una funzione con il nome dell'opzione di menu.Conclusione
Proviamo a compilare ed eseguire il nostro progetto e proviamo ad eseguire le opzioni di Menu, otterremo il risultato che ci aspettiamo.Una prova che possiamo fare è la seguente: inserire l'opzione Exit in un altro menu, aggiungere un menu, inserire un'opzione direttamente nel mnuMain e vedere se viene eseguita, oppure inserire un opzione inesistente e verificare che cosa succede.
Per fare questi test, non ci servirà ricompilare il programma, basterà agire sui file XML dei nostri menu.
Codice sorgente
Il codice sorgente degli esempi che corredano questo articolo è scaricabile dall'Area DownloadFeedback
Per commenti, richieste di chiarimenti, correzioni, su quanto esposto in questo articolo, potete scrivere sul blog dell'autrice.