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
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.

VB
  Imports 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

VB
  Public 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.

VB
  Private 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.

VB
  Public 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à:

VB
  Public 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.

VB
  Public 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.

VB
  Imports 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.

VB
  Public 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.

VB
  Private 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à:

VB
  Public 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.

VB
  Public 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

VB
  Imports 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.

VB
  Public Class MethodsManager
    ....
  End Class
C#
  public class MethodsManager
  {
    ....
  }

I campi privati della classe e il delegate pubblico:

VB
  Private 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.

VB
  Public 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.

VB
  Public 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.

VB
  Public Overrides Function ToString() As String
    Return Me.MethodList.ToString()
  End Function
C#
  public override string ToString()
  {
    return (this.MethodList.ToString());
  }

Il Metodo ExecuteMethod:

VB
  Public 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:

VB
  Imports 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:

VB
  Public 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:

VB
  Public 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>&amp;File</Description>
      <Key>mnuFile</Key>
    </MenuItem>
    <MenuItem>
      <Description>&amp;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>&amp;Impostazioni di Stampa</Description>
      <Key>fnzImpostaStampa</Key>
    </MenuItem>
    <MenuItem>
      <Description>&amp;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 &amp;Uno</Description>
      <Key>fnzUno</Key>
    </MenuItem>
    <MenuItem>
      <Description>Metodo &amp;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:

Di nuovo FrmMain
Andiamo ora a modificare la nostra FrmMain per inserirvi il necessario alle nostre prove:

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.

VB
  Private 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:

VB
  Private 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.

VB
  Private 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:

VB
  Private 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 Download

Feedback
Per commenti, richieste di chiarimenti, correzioni, su quanto esposto in questo articolo, potete scrivere sul blog dell'autrice.