I Delegates e il loro uso al di fuori della gestione eventi - prima parte
a cura di Sabrina Cosolo (requisiti: voglia di impazzire)

Premessa
Alzi la mano chi sa che cos'è un Delegate e chi ne ha fatto uso in qualcosa che non fosse la definizione di un evento personalizzato all'interno di una classe.
Sono sicura che siete in pochi, perciò ho deciso di provare a realizzare un piccolo progetto che permetta a chi lo legge di scoprire alcune delle possibilità che i Delegate ci offrono.

Che cosa realizzeremo in questo progetto?
Un menu parametrico, una delle funzionalità più semplici da ottenere tramite i delegate.
E se vi state chiedendo "parametrico in che senso" e soprattutto "cosa ci faccio con un menu parametrico", le risposte sono: Parametrico=Configurabile dall'esterno dell'applicazione. Cosa farne=Permettere a chi installa oppure a chi usa la vostra applicazione, di configurare le opzioni di menu come preferisce.
Non vi sembra utile? Non sapreste cosa farvene? Mi spiace, comunque provate ad arrivare in fondo a questo articolo e magari in futuro idee sul suo uso potranno venirvi...

Cos'è un delegate?
Prima di illustrarvi la struttura del progetto che realizzeremo, bisogna rispondere a questa prima domanda: un delegate è un puntatore ad un metodo, così come un "reference" (o meglio una variabile di tipo reference) è un puntatore ad un oggetto.

A cosa mi serve un puntatore a metodo? Nel caso degli eventi che siamo abituati ad utilizzare in .NET, la risposta è semplice, ad un delegate assegno l'indirizzo del metodo di gestione dell'evento e quando questo viene scatenato, il mio metodo viene eseguito. Pertanto il loro uso principale è questo, permetterci di utilizzare metodi diversi in base a quelli che possono essere i valori assunti da un parametro, o una serie di parametri, senza per questo dover complicare troppo il codice.

Vi ho confuso ancor di più? Perfetto, allora proviamo a passare al nostro progetto, chissà che non riusciamo a mettere ordine nel caos.
Pure utilizzando in modo esclusivo il C# all'interno dei progetti che realizzo professionalmente, siccome questo sito si chiama Visual Basic Tips & Tricks e siccome io considero sciocche le polemiche relative al "Meglio VB di C#" e viceversa, per par condicio ho scritto l'articolo e l'esempio in entrambi i linguaggi, così da mostrare quanto affermato da Microsoft, che dice che i linguaggi di alto livello sono equivalenti. Per la realizzazione del progetto abbiamo utilizzato Visual Studio 2005 Professional con il framework 2.0.
Credo comunque non vi siano problemi a realizzarlo usando qualsiasi altra versione di Visual Studio 2005.

Prima di continuare, desidero precisare che quanto qui inserito è un esempio che spero possa tornare utile a chi lo leggerà, non pretende di essere il migliore modo per realizzare questo tipo di soluzione e quanto vi è contenuto è stato creato in base agli standard di programmazione che chi scrive utilizza, con i pregi e i difetti che ne conseguono. Anche in questo caso, molte cose nello stile di programmazione sono scelte del tutto personali e quindi opinabili, pertanto usatele come suggerimenti o buttatele via, in base a quello che preferite poi nel lavoro reale.

Lo schema della soluzione Visual Studio di cui troverete il codice da scaricare in Area Downlod (vedi link in fondo all'articolo) è il seguente:

Come possiamo vedere, all'interno della soluzione VbTipsDelegates, ci sono 3 Progetti:

  • HelperClasses - Classi di supporto per la serializzazione, esclusivamente in C#, non discusse nel presente articolo.
  • VbTipsDelegatesCs - Applicazione Windows in C# contenente la realizzazione del progetto.
  • VbTipsDelegatesVb - Applicazione Windows in VB Contenente la realizzazione del progetto.

Il progetto, consta di 8 Classi che listiamo nell'ordine in cui le realizzeremo:

  1. Program - Classe contenente l'entry point del programma, ovvero il metodo Main.
  2. MenuItem - Classe che implementa una opzione di menu
  3. MenuItems - Classe che implementa una collezione di opzioni di menu
  4. Menu - Classe che implementa una struttura Menu completa
  5. MenuMethod - Classe che implementa il contenitore per un Delegate da collegare a menu
  6. MenuMethods - Classe che implementa una collezione di contenitori per i Delegate
  7. MethodsManager - Classe che fornisce al programma la collezione dei delegate e il metodo per eseguirli
  8. FrmMain - Classe Form che ci permette di implementare e testare l'uso del menu parametrico.

Oltre alle classi, abbiamo il file XML che rappresenta il menu parametrico che per semplicità è stato memorizzato in mnuMain.xml, il cui contenuto è il seguente:

  <?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>Prima opzione</Description>
        <Key>fnzUno</Key>
      </MenuItem>
      <MenuItem>
        <Description>Seconda opzione</Description>
        <Key>fnzDue</Key>
      </MenuItem>
    </Items>
  </Menu>

La classe serializzabile Menu e i suoi componenti, MenuItems e MenuItem producono questo file come risultato della loro serializzazione.
Quindi se siete interessati agli argomenti: Classi Serializzabili, Collezioni generics, Collezioni Serializzabili, questo articolo potrebbe tornarvi utile.

Creiamo la nostra soluzione:
Una nota: io utilizzo la versione inglese di Visual Studio, pertanto i nomi dei menu e delle opzioni di Visual Studio sono in inglese; inoltre, siccome mi piace vedere sempre la soluzione che include i miei progetti, andando in Tools-> Options -> Projects and Solutions -> General il mio sistema ha l'opzione Always Show Solution selezionata.

Apriamo Visual Studio .NET e selezioniamo File -> New Project; nella finestra che appare selezioniamo Other project Types; espandiamo il tree e selezioniamo Visual Studio Solution, quindi Blank Solution nella lista delle soluzioni ed infine selezioniamo la cartella del nostro disco ove la soluzione dovrà essere creata (ricordo che verrà generata una cartella col nome della soluzione) e chiamiamo la soluzione VbTipsDelegates.

Diamo OK, e verranno create la cartella e la soluzione.

Per prima cosa, visto che in questo articolo non lo discuteremo - semplicemente perché queste classi sono state realizzate usando quanto indicato dall'articolo sulla serializzazione oggetti pubblicato lo scorso anno da Enrico Barillari su questo sito con le modifiche date dalle opzioni del framework 2.0 - dallo zip contenente tutto il progetto allegato a questo articolo, andiamo a copiare la cartella HelperClasses e incolliamola all'interno della cartella della nostra soluzione. In questo progetto vi sono 2 classi, che servono per fornire il metodo di serializzazione su file e serializzazione su stringa di una classe qualsiasi.

Dopo aver copiato la cartella, nel solution explorer, facciamo click con il tasto destro del mouse sulla soluzione e scegliamo: Add -> Existing project nella dialog di selezione, spostiamoci sulla cartella della nostra soluzione, e apriamo la cartella HelperClasses, selezionando HelperClasses.csproj, il progetto che abbiamo appena copiato. Clicchiamo il pulsante Open e il progetto sarà aggiunto alla nostra soluzione.

Adesso siamo pronti a cominciare a creare il nostro progetto vero e proprio.
Nel solution explorer clicchamo nuovamente col tasto destro sulla soluzione e selezioniamo Add -> New Project; in base alle nostre preferenze, scegliamo VB o C# e un progetto di tipo Windows Application; chiamiamolo VbTipsdelegatesCs o VbTipsDelegatesVb e diamo OK, verrà generato un progetto con una singola form.
Facciamo Click sul pulsante "Show all Files" che appare sulla toolbar del solution explorer quando selezioniamo il progetto:

VB
C#

Cliccando questo pulsante, tutto ciò che solitamente è nascosto viene reso visibile. Nascondere parte dei files di un progetto viene usato come metodo per rendere le cose più facili ai principianti, ma non sempre, a mio avviso, ciò costutisce una vera facilitazione (però rimarco che questa è solo una mia opinione). Ora, per fare in modo che l'applicazione VB sia uguale a quella C#, (e per imparare qualcosa di nuovo e di tanto utile [nota del revisore]) procederemo a fare alcune modifiche ad entrambe.

Doppio click sulla cartella My Project, doppio click sulla cartella Properties

VB
C#

Togliamo il Check Su Enable Application Framework;
su Startup object selezioniamo Sub Main (e non importa se appare un errore che dice che la Sub Main non è stata trovata, fra poco l'aggiungeremo);
nella casella Root Namespace, sostituiamo VbTipsDelegatesVb con VbTips

Nella casella Default Namespace sostituiamo VbTipsDelegatesCs con VbTips
Clicchiamo con il tasto destro sul progetto, sul menu selezioniamo Add -> Class;
chiamiamo la classe Program.vb e diamo OK.
Clicchiamo con il tasto destro sul progetto, sul menu selezioniamo Add -> Class;
chiamiamo la classe Program.cs e diamo OK.

VB
 

All'interno della classe, inseriamo il seguente codice:

  Imports System
  Imports System.Collections.Generic
  Imports System.Windows.Forms
 
  Public Class Program
    Public Shared Sub Main()
		  Application.EnableVisualStyles()
		  Application.SetCompatibleTextRenderingDefault(False)
		  Application.Run(New FrmMain())
    End Sub
  End Class

Abbiamo così generato la nostra Sub Main che mancava e reso il progetto VB equivalente al progetto C#.

Visual Basic, a differenza di C#, utilizza il Root Namespace inserito nella Pagina Application di My Project senza visualizzarlo in modo esplicito, (una delle cose che, a mio avviso, invece di semplificare la vita, la complicano: infatti, se non consultasse My project, uno non saprebbe qual è il Namespace in cui si trovano le classi che sta scrivendo).

C#
 

All'interno della classe, inseriamo il seguente codice:

  using System;
  using System.Collections.Generic;
  using System.Windows.Forms;

  
  namespace VbTipsDelegatesCs
  {
    static class Program
    {
      [STAThread]
      static void Main()
      {
	  		Application.EnableVisualStyles();
	  		Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
      }
    }
  }  

Selezioniamo VBTipsDelegatesCs (sulla riga namespace) e premiamo <Ctrl>+<H> per aprire la finestra Find and Replace;
troveremo la stessa parola nel campo Find what: inseriamo VbTips nella casella Replace with:
selezioniamo Current Project nella combobox Look in:
usiamo il bottone Replace per sostituire il Namespace corretto in Program.cs, FrmMain.cs, FrmMain.designer.cs e solo in questi files.

Struttura delle classi
Per costruire un menu parametrico che ci permetta, tramite un file di testo esterno (il file XML altro non è), di decidere le opzioni del nostro menu, abbiamo bisogno di qualcosa che ci permetta di gestire questo tipo di oggetto. Per farlo utilizzeremo tre classi, che potremmo definire "concentriche": la classe Menu, che gestisce il menu, gli fornisce un nome univoco, un titolo e una lista di opzioni; la classe MenuItems, che fornisce la lista di opzioni del menu; la classe MenuItem che fornisce la singola opzione di menu.

Riferimenti
Prima di procedere, selezioniamo la cartella References, facciamo click col tasto destro, selezioniamo Add Reference e nella dialog proposta scegliamo il tab Projects, ci appariranno tutti i progetti della soluzione. Selezioniamo HelperClasses e diamo OK.

Questa operazione, permetterà ai nostri progetti di utilizzare le 2 classi per la serializzazione che si trovano nella Dll HelperClasses.

Ora iniziamo a vedere il codice per la gestione delle nostre classi, per semplicità e comprensibilità, le varie componenti inserite nel codice, saranno visualizzate a blocchi, quindi prima il Namespace, poi la classe, i campi, le proprietà, il costruttore, i metodi.

MenuItem
Questa è la classe che definisce un'opzione di menu, pertanto un elemento della collezione delle opzioni di menu.
Per generare la classe facciamo click con il tasto destro sul progetto nel solution explorer selezioniamo Add -> Class e chiamiamo la classe MenuItem.

Namespaces importati:

VB
 
Imports System
Imports System.Text
Imports System.Xml.Serialization
C#
 
  using System;
  using System.Text;
  using System.Xml.Serialization;

System.Text ospita lo StringBuilder che utilizzeremo per comporre le stringhe del metodo ToString
System.XmlSerialization ci serve per l'attributo di serializzazione XmlRoot che usiamo per standardizzare i files prodotti dalle nostre applicazioni.

Namespace della classe:

VB
 

In VB, visto che utilizziamo un solo livello di namespace, non dobbiamo fare nulla.

C#
 
  namespace VbTips
  {
    ...
  }

A differenza di VB, in cui il Namespace viene automaticamente impostato uguale al Root Namespace definito nelle proprietà di progetto (My project), in C# il Default Namespace impostato per il progetto è solamente un valore di default che il generatore di classi di Visual Studio imposta all'interno della nostra classe.
Se, per un verso, questo sembra complicare un po' il codice, e, se cambiato, costringe a un Find and Replace, per un altro verso è utile, in quanto possiamo immediatamente sapere dove una classe è collocata e in un progetto si potrebbero avere classi con Root namespace diverso. Questo non toglie che, impostando il Root Namespace di VB a una stringa vuota e inserendo la clausola Namespace nelle sue classi, si ottenga lo stesso risultato.

La definizione della classe:

VB
 
  <Serializable(), XmlRoot(Namespace:="http://www.visual-basic.it")> _
  Public Class MenuItem
    ...
  End Class
C#
 
  [Serializable, XmlRoot(Namespace = "http://www.visual-basic.it")]
  public class MenuItem
  {
    ...
  }

Come vediamo, abbiamo aggiunto alla definizione della classe l'attributo Serializable e l'attributo XmlRoot.
Questi due attributi, informano il sistema sul fatto che il contenuto della classe può essere reso persistente tramite serializzazione e per la serializzazione di tipo XML impostano un Namespace in cui inserire la classe per renderla univoca ad ogni livello, quindi predisporla per l'uso tramite webservices (ma quest'ultimo argomento esula da questo articolo).

I campi privati della classe:

VB
 
  Private mClassName As String = _
    System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name
  Private Shared mClassNameS As String = _
    System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name
 
  Private mKey As String
  Private mDescription As String 
C#
 
  private readonly string mClassName =
    System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name;
  private readonly static string mClassNameS =
    System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name;
 
  private string mDescription;
  private string mKey;

Come potete notare, ho inserito due strani campi, mClassName e mClassNameS, readonly e con un valore piuttosto strano; ebbene, questi due campi non hanno altro uso all'interno della nostra classe che quello di restituire il nome della classe stessa e vengono utilizzati all'interno della gestione delle eccezioni, il loro uso non è obbligatorio, è solo un metodo che ho adottato all'interno dei miei progetti e che trovo piuttosto utile: infatti, inserendo il nome della classe all'interno della gestione delle eccezioni, il debug è molto, molto più comodo. L'utente finale, infatti, può informarci direttamente sul "luogo" in cui l'eccezione ha origine. Troveremo questi campi all'interno di tutte le classi del progetto.

Gli altri campi, mKey e mDescription, rispecchiano le rispettive proprietà e sono i veri e propri dati di questa classe. Come potete notare, un'altro vezzo nel mio modo di scrivere codice è chiamare tutte le variabili private, che sono anche dette variabili Member di una classe con il prefisso m e di scriverle in camelCase. Le property così avranno lo stesso nome privato della lettera emme e in PascalCase. Alcuni, al posto della m usano _ (underscore), io non lo faccio solo perché è un tasto che mi sta antipatico, nulla di più.

Il costruttore:

VB
 
  Public Sub New()
     Me.mKey = String.Empty
     Me.mDescription = String.Empty
  End Sub
C#
 
  public MenuItem()
  {
    this.mKey = string.Empty;
    this.mDescription = string.Empty;
  }

Le proprietà:
Inizializziamo i campi della classe nel costruttore.

VB
 
  Public Property Key() As String
     Get
          Return (Me.mKey)
     End Get
     Set(ByVal value As String)
          Me.mKey = value
     End Set
  End Property
 
  Public Property Description() As String
     Get
          Return (Me.mDescription)
     End Get
     Set(ByVal value As String)
          Me.mDescription = value
     End Set
  End Property
C#
 
  public string Key
  {
     get
     {
          return (this.mKey);
     }
     set
     {
          this.mKey = value;
     }
  }
 
  public string Description
  {
     get
     {
          return (this.mDescription);
     }
     set
     {
          this.mDescription = value;
     }
  }

non ha nulla di complesso, posso solo annotare che VB usa meno righe ma scrive molto di più di C# :o)

Il metodo ToString:

VB
 
  Public Overrides Function ToString() As String
     Try
          Dim sb As StringBuilder = New StringBuilder()
          sb.Append(Me.mKey)
          sb.Append(";")
          sb.Append(Me.mDescription)
          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();
          sb.Append(this.mKey);
          sb.Append(";");
          sb.Append(this.mDescription);
          return (sb.ToString());
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + mClassName + "."
          + System.Reflection.MethodBase.GetCurrentMethod().Name
          + ": " + ex.Message, ex);
     }
  }

Questo metodo ci serve per rappresentare il contenuto della classe sotto forma di stringa; questa forma può essere quella che preferite; in questo caso, per semplicità, ho composto una stringa CSV key;description, che magari potrebbe servirci in seguito.
Faccio notare la gestione eccezioni che uso in tutte le mie classi non appartenenti ad elementi visuali come le form o gli user control. Una eventuale eccezione intercettata dal metodo della classe, viene rimandata al livello precedente dello stack di chiamata, arricchita del nome della classe e del nome del metodo in cui si è verificata. Inoltre, l'eccezione originale viene inserita nel parametro che diverrà la InnerException del livello precedente. In questo modo, è possibile scavare attraverso le eccezioni per trovare i problemi.
Ovviamente questa è una gestione eccezioni generica: in metodi ove possano verificarsi tipi specifici di eccezioni, sarà opportuno gestire per quanto possibile tutte le eccezioni specifiche.

Il metodo di serializzazione:

VB
 
  Public Sub WriteXml(ByVal pXmlPath As String)
     Try
          Dim xSer As ObjSerializer = New ObjSerializer()
          xSer.SerializeToFile(pXmlPath, Me, GetType(MenuItem))
     Catch ex As Exception
          Throw New ApplicationException(" " & Me.mClassName & "." _
          & System.Reflection.MethodBase.GetCurrentMethod().Name _
          &": "& ex.Message, ex)
     End Try
  End Sub
 
  Public Function WriteXml() As String
     Try
          Dim xSer As ObjSerializer = New ObjSerializer()
          Return (xSer.SerializeToString(Me))
     Catch ex As Exception
          Throw New ApplicationException(" " & Me.mClassName & "." _
          & System.Reflection.MethodBase.GetCurrentMethod().Name _
          & ": " & ex.Message, ex)
     End Try
  End Function
C#
 
  public void WriteXml(string pXmlPath)
  {
     try
     {
          ObjSerializer xSer = new ObjSerializer();
          xSer.SerializeToFile(pXmlPath, this, typeof(MenuItem));
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + mClassName + "."
          + System.Reflection.MethodBase.GetCurrentMethod().Name
          + ": " + ex.Message, ex);
     }
  }
 
  public string WriteXml()
  {
     try
     {
          ObjSerializer xSer = new ObjSerializer();
          return (xSer.SerializeToString(this));
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + mClassName + "."
          + System.Reflection.MethodBase.GetCurrentMethod().Name
          + ": " + ex.Message, ex);
     }
  }

Il metodo di Serializzazione, come possiamo notare ha due overload, ovvero due versioni, in base al fatto che richiediamo di serializzare la classe su File oppure su una stringa. il metodo di serializzazione genera, a partire dalla nostra classe, un file o una stringa Xml che ne rappresenta il contenuto. Questi prodotti di output, se passati al metodo di Deserializzazione, che vedremo tra breve, danno luogo ad una classe equivalente a quella serializzata.

Il metodo di deserializzazione:

VB
 
  Public Shared Function ReadXml(ByVal pXml As String _
    , ByVal pIsXmlData As Boolean) As MenuItem
     Try
          Dim retItem As MenuItem

          Dim xSer As ObjSerializer = New ObjSerializer()
          If (pIsXmlData) Then
               retItem = _
               CType(xSer.DeserializeFromString(GetType(MenuItem) _
               , pXml), MenuItem)
          Else
               retItem = _
               CType(xSer.DeserializeFromString(GetType(MenuItem) _
               , pXml), MenuItem)
          End If
          Return (retItem)
     Catch ex As Exception
          Throw New ApplicationException(" " & Me.mClassName & "." & _
          System.Reflection.MethodBase.GetCurrentMethod().Name & ": " _
          & ex.Message, ex)
     End Try
  End Function
C#
 
  public static MenuItem ReadXml(string pXml, bool pIsXmlData)
  {
     MenuItem retItem = null;
     try
     {
          ObjSerializer xSer = new ObjSerializer();
          if (pIsXmlData)
          {
      retItem =
      xSer.DeserializeFromString(typeof(MenuItem)
      , pXml) as MenuItem;
          }
          else
          {
      retItem =
      xSer.DeserializeFromString(typeof(MenuItem)
      , pXml) as MenuItem;
          }
     }
     catch (Exception ex)
     {
     throw new ApplicationException(" " + mClassNameS + "."
     + System.Reflection.MethodBase.GetCurrentMethod().Name
     + ": " + ex.Message, ex);
     }
     return (retItem);
  }

In questo caso, non è stato possibile creare 2 metodi in overload, perché sia la deserializzazione da File che quella da stringa hanno il medesimo tipo di parametro (una stringa) e il medesimo tipo di ritorno (una classe MenuItem). A differenza del metodo di serializzazione, che deve essere chiamato da un'instanza della classe stessa in quanto ha bisogno di un oggetto da serializzare, questo metodo è stato definito come static (C#) o shared (VB) modificatore che permette di utilizzarla senza instanziare una classe. Vedremo come questo approccio ai metodi di serializzazione, ci permette di lavorare in modo semplice quando li utilizzeremo.

MenuItems
Questa classe eredita dalla collezione Generic List, e ci permette, senza dover scrivere una sola riga di codice, di creare una collezione tipizzata di MenuItem. Non sempre si ha necessità di implementare una apposita classe, quando si usa una collezione costruita ereditando da una Generic List; in questo caso, ci serve, perché vogliamo aggiungere il metodo ToString e i metodi di serializzazione specifici della classe stessa.

Per generare la classe, facciamo click con il tasto destro sul progetto nel solution explorer e selezioniamo Add -> Class, chiamiamo la classe MenuItems e diamo OK.

Namespaces:

VB
 
  Imports System
  Imports System.Collections.Generic
  Imports System.Text
  Imports System.Xml.Serialization
C#
 
  using System;
  using System.Collections.Generic;
  using System.Text;
  using System.Xml.Serialization;
  
  namespace VbTips
  {
    ...
  }

Rispetto alla classe precedente, abbiamo aggiunto il namespace System.Collections.Generic, che ospita la classe List da cui la nostra collection eredita

La definizione della classe:

VB
 
  <Serializable(), XmlRoot(Namespace:="http://www.visual-basic.it")> _
  Public Class MenuItems
     Inherits List(Of MenuItem)
     ...
  End Class
C#
 
  [Serializable, XmlRoot(Namespace = "http://www.visual-basic.it")]
  public class MenuItems : List<MenuItem>
  {
    ...
  }

Oltre agli attributi per la serializzazione, abbiamo aggiunto la direttiva che indica che la nostra classe eredita dalla classe Generic List ed implementa una Lista di MenuItem.

I Campi Privati della classe:

VB
 
  Private mClassName As String =_
    System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name
  Private Shared mClassNameS As String =
    System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name
C#
 
  private readonly string mClassName =
    System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name;
  private readonly static string mClassNameS =
    System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name;

Essendo la nostra classe una collezione tipizzata di MenuItem, non abbiamo bisogno di campi, a parte i due campi Helper per la gestione errori che come già detto troveremo in tutte le classi implementate.

Il Costruttore:
In questo caso, non avendo alcun campo da inizializzare ne alcun oggetto da instanziare, non ci serve un costruttore, basta quello della classe List che ci viene automaticamente fornito.

La definizione delle Proprietà:
Non essendoci altri campi, oltre alla lista fornita dalla classe base, non ci sono proprietà specifiche della collection.

Il metodo ToString:

VB
 
  Public Overrides Function ToString() As String
     Try
          Dim sb As StringBuilder = New StringBuilder(String.Empty)
          For Each item As MenuItem In Me
               sb.Append(item.ToString())
               sb.AppendLine()
          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();
          foreach (MenuItem mnu in this)
          {
               sb.Append(mnu.ToString());
               sb.AppendLine();
          )
          return (sb.ToString());
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + mClassName + "."
               + System.Reflection.MethodBase.GetCurrentMethod().Name
               + ": " + ex.Message, ex);
     }
  }

Tutto molto semplice, viene usato uno StringBuilder per comporre una lista dei MenuItem contenuti nella collection.

Il metodo di Serializzazione:

VB
 
  Public Sub WriteXml(ByVal pXmlPath As String)
     Try
          Dim xSer As ObjSerializer = New ObjSerializer()
          xSer.SerializeToFile(pXmlPath, Me, GetType(MenuItems))
     Catch ex As Exception
          Throw New ApplicationException(" " _
          & Me.mClassName & "." _
          & System.Reflection.MethodBase.GetCurrentMethod().Name _
          & ": " & ex.Message, ex)
     End Try
  End Sub
 
  Public Function WriteXml() As String
     Try
          Dim xSer As ObjSerializer = New ObjSerializer()
          Return (xSer.SerializeToString(Me))
     Catch ex As Exception
          Throw New ApplicationException(" " _
          & Me.mClassName & "." _
          & System.Reflection.MethodBase.GetCurrentMethod().Name _
          & ": " & ex.Message, ex)
     End Try
  End Function
C#
 
  public void WriteXml(string pXmlPath)
  {
     try
     {
          ObjSerializer xSer = new ObjSerializer();
          xSer.SerializeToFile(pXmlPath, this, typeof(MenuItems));
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + mClassName + "."
               + System.Reflection.MethodBase.GetCurrentMethod().Name
               + ": " + ex.Message, ex);
     }
  }
 
  public string WriteXml()
  {
     try
     {
          ObjSerializer xSer = new ObjSerializer();
          return (xSer.SerializeToString(this));
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + mClassName + "."
               + System.Reflection.MethodBase.GetCurrentMethod().Name
               + ": " + ex.Message, ex);
     }
  }

Come per la classe del singolo elemento, anche per la classe dell'insime abbiamo predisposto due metodi di serializzazione, uno che genera un file e l'altro che genera una stringa. Come potete notare dal codice scritto, grazie alla nostra classe helper ObjSerializer, non c'è nulla di diverso fra la serializzazione di un MenuItem e quella di una MenuItems. Abbiamo solo aggiunto, per sicurezza, alle informazioni sulla classe in serializzazione, anche quelle relative al tipo di oggetto in essa contenuto.

Il metodo di Deserializzazione:

VB
 
  Public Shared Function ReadXml _
    (ByVal pXml As String, ByVal pIsXmlData As Boolean) As MenuItems

     Try
          Dim retItem As MenuItems

          Dim xSer As ObjSerializer = New ObjSerializer()
          If (pIsXmlData) Then

               retItem = _
                   CType(xSer.DeserializeFromString(GetType(MenuItems) _
                    , pXml), MenuItems)
          Else
               retItem = _
                   CType(xSer.DeserializeFromString(GetType(MenuItems) _
                    , pXml), MenuItems)
          End If
          Return (retItem)
     Catch ex As Exception
          Throw New ApplicationException(" " _
          & mClassNameS & "." _
          & System.Reflection.MethodBase.GetCurrentMethod().Name _
          & ": " & ex.Message, ex)
     End Try
  End Function
C#
 
  public static MenuItems ReadXml(string pXml, bool pIsXmlData)
  {
     MenuItems retItem = null;
     try
     {
          ObjSerializer xSer = new ObjSerializer();
          if (pIsXmlData)
          {
               retItem =
                   xSer.DeserializeFromString(typeof(MenuItems)
                    , pXml) as MenuItems;

          }
          else
          {
               retItem =
                   xSer.DeserializeFromString(typeof(MenuItems)
                    , pXml) as MenuItems;
          }
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + mClassNameS + "."
               + System.Reflection.MethodBase.GetCurrentMethod().Name
               + ": " + ex.Message, ex);
     }
     return (retItem);
  }

Anche per la deserializzazione, possiamo notare che non c'è alcuna differenza fra il codice necessario per deserializzare un elemento e quello per deserializzare una collezione.

Menu
Questa classe implementa il contenitore ove inserire le opzioni di menu e aggiunge gli ulteriori elementi che potranno poi permetterci di costruire una serie di menu parametrici strutturati.

Per generare la classe, facciamo click con il tasto destro sul progetto nel solution explorer e selezioniamo Add -> Class, chiamiamo la classe Menu e diamo OK.

NameSpaces:

VB
 
  Imports System
  Imports System.Text
  Imports System.Xml.Serialization
C#
 
  using System;
  using System.Text;
  using System.Xml.Serialization;
  
  namespace VbTips
  {
    ...
  }

I namespace utilizzati sono gli stessi della classe MenuItem, infatti questa classe è simile alla classe MenuItem nella forma e funzionalità.

La definizione della classe:

VB
 
  &Serializable(), XmlRoot(Namespace:="http://www.visual-basic.it")& _
  Public Class Menu
    ...
  End Class
C#
 
  [Serializable, XmlRoot(Namespace = "http://www.visual-basic.it")]
  public class Menu
  {
    ...
  }

Come per MenuItem, indichiamo esplicitamente che la classe è serializzabile.

I campi privati della classe:

VB
 
  Private mClassName As String = _
     System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name
  Private Shared mClassNameS As String = _
     System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name
  
  Private mKey As String
  Private mTitle As String
  Private mItems As MenuItems
C#
 
  private readonly string mClassName =
     System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name;
  private readonly static string mClassNameS =
     System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name;
  
  private MenuItems mItems;
  private string mTitle;
  private string mKey;

Dal punto di vista dei dati contenuti, la sola differenza annotabile è che mItems è una collezione, ma, dal punto di vista formale, Menu è un contenitore di dati, esattamente come MenuItem.

Il Costruttore:
Inizializziamo i campi dati.

VB
 
  Public Sub New()
    Me.mKey = String.Empty
    Me.mTitle = String.Empty
    Me.mItems = New MenuItems()
  End Sub
C#
 
  public Menu()
  {
    this.mItems = new MenuItems();
    this.mTitle = string.Empty;
    this.mKey = string.Empty;
  }

La definizione delle Proprietà:
Esponiamo pubblicamente i dati all'esterno della classe.

VB
 
  Public Property Key() As String
     Get
          Return mKey
     End Get
     Set(ByVal value As String)
          mKey = value
     End Set
  End Property
  
  Public Property Title() As String
     Get
          Return mTitle
     End Get
     Set(ByVal value As String)
          mTitle = value
     End Set
  End Property
  
  Public Property Items() As MenuItems
     Get
          Return mItems
     End Get
     Set(ByVal value As MenuItems)
          mItems = value
     End Set
  End Property
C#
 
  public string Key
  {
     get
     {
          return mKey;
     }
     set
     {
          mKey = value;
     }
  }
  
  public string Title
  {
     get
     {
          return mTitle;
     }
     set
     {
          mTitle = value;
     }
  }
  
  public MenuItems Items
  {
     get
     {
          return mItems;
     }
     set
     {
          mItems = value;
     }
  }

Il metodo ToString:
visualizza il nostro menu utilizzando il metodo base fornito dai suoi componenti.

VB
 
  Public Overrides Function ToString() As String
     Try
          Dim sb As StringBuilder = New StringBuilder(String.Empty)
          sb.AppendFormat("Menu ({0}): {1}", Me.Key, Me.Title)
          sb.AppendLine()
          sb.AppendLine(Me.Items.ToString())
          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);
          sb.AppendFormat("Menu ({0}): {1}", this.Key, this.Title);
          sb.AppendLine();
          sb.AppendLine(this.Items.ToString());
          return (sb.ToString());
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + mClassName + "."
               + System.Reflection.MethodBase.GetCurrentMethod().Name
               + ": " + ex.Message, ex);
     }
  }

Il metodo di Serializzazione:

VB
 
  Public Sub WriteXml(ByVal pXmlPath As String)
     Try
          Dim xSer As ObjSerializer = New ObjSerializer()
          xSer.SerializeToFile(pXmlPath, Me, GetType(Menu) _
          , New Type() {GetType(MenuItems), GetType(MenuItem)})
     Catch ex As Exception
          Throw New ApplicationException(" " _
          & Me.mClassName & "." _
          & System.Reflection.MethodBase.GetCurrentMethod().Name _
          & ": " & ex.Message, ex)
     End Try
  End Sub
 
  Public Function WriteXml() As String
     Try
          Dim xSer As ObjSerializer = New ObjSerializer()
          Return (xSer.SerializeToString(Me,  _
          New Type(){GetType(MenuItems), GetType(MenuItem)}))
     Catch ex As Exception
          Throw New ApplicationException(" " _
          & Me.mClassName & "." _
          & System.Reflection.MethodBase.GetCurrentMethod().Name _
          & ": " & ex.Message, ex)
     End Try
  End Function
C#
 
  public void WriteXml(string pXmlPath)
  {
     try
     {
          ObjSerializer so = new ObjSerializer();
          so.SerializeToFile(pXmlPath, this, typeof(Menu)
          , new Type[] { typeof(MenuItems), typeof(MenuItem) });
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + this.mClassName + "."
               + System.Reflection.MethodBase.GetCurrentMethod().Name
               + " " + ex.Message, ex);
     }
  }
  public string WriteXml()
  {
     try
     {
          ObjSerializer so = new ObjSerializer();
          return (so.SerializeToString(this
          , new Type[] { typeof(MenuItems), typeof(MenuItem) }));
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + this.mClassName + "."
               + System.Reflection.MethodBase.GetCurrentMethod().Name
               + " " + ex.Message, ex);
     }
  }

Il metodo di serializzazione, è anche in questo caso simile a quello della classe MenuItem. La sola modifica è l'array di Tipi che passiamo al metodo per indicare quali sono gli oggetti serializzabili contenuti nella classe affinché il metodo automatico di serializzazione abbia tutte le informazioni necessarie a serializzare correttamente tutti i componenti.

Anche per il metodo di serializzazione, indichiamo i componenti della classe per la loro corretta deserializzazione.

Il metodo di Deserializzazione:

VB
 
  Public Shared Function ReadXml(ByVal pXml As String _
                               , ByVal pIsXmlData As Boolean) As Menu
     Try
          Dim retItem As Menu

          Dim xSer As ObjSerializer = New ObjSerializer()
          If (pIsXmlData) Then

               retItem = _
               CType(xSer.DeserializeFromString(GetType(Menu) _
               , New Type() {GetType(MenuItems), GetType(MenuItem)} _
               , pXml), Menu)
          Else
               retItem = _
               CType(xSer.DeserializeFromFile(GetType(Menu) _
               , New Type() {GetType(MenuItems), GetType(MenuItem)} _
               , pXml), Menu)
          End If
          Return (retItem)
     Catch ex As Exception
          Throw New ApplicationException(" " _
          & mClassNameS & "." _
          & System.Reflection.MethodBase.GetCurrentMethod().Name _
          & ": " & ex.Message, ex)
     End Try
  End Function
C#
 
  public static Menu ReadXml(string pXml, bool pIsXmlData)
  {
     Menu ret = null;
     try
     {
          ObjSerializer so = new ObjSerializer();
          if (!pIsXmlData)
          {
               ret = (Menu)so.DeserializeFromFile(typeof(Menu)
                   , new Type[] { typeof(MenuItems), typeof(MenuItem) }, pXml);

          } // if
          else
          {
               ret = (Menu)so.DeserializeFromString(typeof(Menu)
                   , new Type[] { typeof(MenuItems), typeof(MenuItem) }, pXml);
          }
     }
     catch (Exception ex)
     {
          throw new ApplicationException(" " + mClassNameS + "."
               + System.Reflection.MethodBase.GetCurrentMethod().Name
               + " " + ex.Message, ex);
     }
     return (ret);
  }

Conclusione
Con questa classe, abbiamo concluso lo sviluppo della prima parte del progetto, quindi le classi che ci permetteranno di generare il menu parametrico, di salvarlo su un file XML su disco o su un campo NText in un database, usando la serializzazione, nonché di recuperare in seguito le informazioni per ricostruire l'oggetto menu nel nostro programma.

Nella seconda parte, passeremo a sviluppare le cose più interessanti, realtivamente all'oggetto di cui ci occupiamo, i Delegate.

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