Classi e OOP introduzione (parte 4/4)
a cura di Enrico Barillari e Sabrina Cosolo (requisiti: minima esperienza di programmazione)

Una classe Collection, il Team Programmatori:
Abbiamo creato una classe, la Persona, e da questa abbiamo generato una classe erede, il Programmatore, adesso iniziamo ad esplorare le possibilità offerte dalla OOP e dal Framework per poter creare qualcosa di più complesso e soprattutto qualcosa di uso comune, come può essere una lista di oggetti, per meglio dire una collezione di oggetti da utilizzare quando un singolo valore non è sufficiente a raggiungere uno scopo.

Nella parte precedente del nostro articolo abbiamo utilizzato una collection di base fra quelle fornite dal Framework .NET senza alcuna particolare modifica, dato che ci serviva per contenere una semplice lista di stringhe.

Adesso vediamo invece come creare una collezione per gestire una lista di programmatori.
Per fare questo, in Visual Studio aggiungiamo al nostro progetto la terza classe e chiamiamola TeamProgrammatori. All'interno della bozza che Visual Studio ci ha preparato, troveremo questo codice:

  using System;
  namespace MyNamespace
  {
    public class TeamProgrammatori
    {
      public TeamProgrammatori()
      {
      }
    } 
  }

L'implementazione di una Interfaccia
Per poter trasformare questa semplice classe in una collection, possiamo agire in due modi, implementare le funzioni che ci servono manualmente, oppure utilizzare ancora una volta l'ereditarietà ma stavolta non quella di Classe ma quella di Interfaccia, implementando per la nostra classe l'interfaccia ICollection fornita dal Framework. Che cos'è una interfaccia? se la classe è un progetto, l'Interfaccia è uno schema che gli si sovrappone, ad esempio la casa è una classe, l'impianto antifurto della casa è un interfaccia, una casa che implementa l'impianto antifurto diviene una casa sicura, un impianto antifurto per essere tale deve fornire determinate caratteristiche, ad esempio una sirena, dei sensori di rilevamento del movimento, un commutatore telefonico eccetera eccetera.

In OOP implementare una interfaccia equivale a sottoscrivere un contratto, che ci impegna ad implementare una serie di metodi e proprietà pubblici con firma ben precisa, ma senza obblighi per quanto riguarda il modo in cui al loro interno la funzionalità viene implementata.
Questo permette al compilatore .Net di risparmiare tempo, sapendo in anticipo dove l'esecuzione del codice prosegue, senza dover cercare un altro metodo; ed al programmatore di creare codice personalizzato, pur mantenendosi all'interno della catena dell'ereditarietà.

L'interfaccia è una classe di tipo particolare, infatti abbiamo visto che la nostra classe base Persona contiene al suo interno Dati e Funzioni che sono ereditate dalla classe Programmatore. L'interfaccia invece essendo solo un contratto contiene al suo interno solo una serie di dichiarazioni di metodi e proprietà che devono essere implementate all'interno della classe che decide di implementare (ereditare) quell'interfaccia.

Le interfacce permettono inoltre di realizzare l'ereditarietà multipla, infatti una classe può essere erede di una sola classe ma implementare più di una interfaccia.
L'importanza delle interfacce in ambiente .Net è sottolineata dallo stresso IDE, nel quale è sufficiente digitare il simbolo duepunti (:) dopo il nome della classe, seguito dal nome dell'interfaccia, e premere il tasto Tab, perché l'editor di codice ci fornisca l'implementazione di base di tutti i metodi necessari.
Da sottolineare che non è necessario che i metodi richiesti facciano realmente qualcosa: è possibile utilizzare solamente i metodi necessari, lasciando che gli altri non facciano niente, il contratto sottoscritto sarà comunque valido, basta che i parametri ed il tipo restituito da ogni metodo corrispondano a quanto previsto dall'interfaccia.
Una interfaccia va utilizzata quando si ritiene di fare adottare ad una classe un certo comportamento, conservando la piena libertà sul modo di implementare il comportamento stesso, oppure quando è necessaria l'ereditarietà multipla, che non è altrimenti realizzabile.

Vediamo cosa succede all'interno della nostra classe se decidiamo di implementare l'interfaccia fornita dal Framework .NET che si chiama ICollection.

Per fare questo per prima cosa dobbiamo aggungere alle clausole using in testata alla nostra classe il riferimento al namespace base che contiene le collection di .NET che guardacaso si chiama System.Collection. Lo aggiungiamo in testata alla classe e poi, dato che anche le interfacce sono classi, per implementare l'interfaccia ICollection digitiamo accanto alla dichiarazione della classe i duepunti e la parola ICollection. A questo punto, Visual Studio ci mostrerà un messaggio che dice:

Premere il tasto TAB (tabulatore) per implementare automaticamente il codice base per l'interfaccia System.Collections.ICollection.
Se premiamo il tabulatore, otterremo che in fondo alla nostra classe appariranno questi segnalini, si tratta di due #region di codice simili a quelle che Visual Studio crea per il codice generato dal Form Designer, se le espandiamo, vedremo al loro interno apparire una serie di proprietà e metodi che devono essere implementati necessariamente affinché la nostra classe sia una collection.
Ma vediamo quale è il codice implementato e a cosa serve.

  public int Count
  {
    get
    {
      // TODO:  Add TeamProgrammatori.Count getter implementation
    return 0;
    }
  }
 
  public void CopyTo(Array array, int index)
  {
    // TODO:  Add TeamProgrammatori.CopyTo implementation
  }

 
  public object SyncRoot
  {
    get
    {
      // TODO:  Add TeamProgrammatori.SyncRoot getter implementation
      return null;
    }
  } 
 
  public bool IsSynchronized
  {
    get
    {
      // TODO:  Add TeamProgrammatori.IsSynchronized getter implementation
      return false;
    }
  }
 
  public IEnumerator GetEnumerator()
  {
    // TODO:  Add TeamProgrammatori.GetEnumerator implementation
    return null;
  }

Spaventati? Non è così complicato come sembra, l'interfaccia ICollection, prevede che la classe che la implementa debba fornire alcune proprietà ed alcuni metodi pubblici che sono i seguenti:

Vediamo come i metodi della collection verranno implementati e scopriremo insieme che è davvero semplice. Ora per prima cosa, inseriamo all'interno della nostra classe una collection di base, esattamente un ArrayList che ci servirà come appoggio per la costruzione della collection.

  public class TeamProgrammatori : ICollection
  {
    private ArrayList mTeam;
 
    public TeamProgrammatori()
    {
      this.mTeam = new ArrayList();
    }
  }   

Abbiamo aggiunto la variabile member ArrayList e la sua inizializzazione all'interno del costruttore. Implementiamo ora le proprietà e i metodi richiesti dall'interfaccia.

  public int Count
  {
    get
    {
      return( this.mTeam.Count );
    }
  }
 
  public void CopyTo(Array pArray, int pIndex)
  {
    this.mTeam.CopyTo( pArray, pIndex );
  }

 
  public object SyncRoot
  {
    get
    {
      return(this);
    }
  }

 
  public bool IsSynchronized
  {
    get
    {
      return(false);
    }
  } 

 
  public IEnumerator GetEnumerator()
  {
    this.mTeam.GetEnumerator();
  }

Se osservate cosa abbiamo fatto per implemetare la proprietà e i due metodi che ci interessavano, scoprirete immediatamente che è così semplice da sembrare un trucco, infatti non abbiamo fatto altro che esporre le proprietà ed i metodi con lo stesso nome della classe collection base che useremo all'interno del nostro TeamProgrammatori. In realtà è solo un implementazione di ereditarietà, un riutilizzo dei componenti base per creare nuovi componenti con nuove funzionalità.

Diamo funzionalità alla classe TeamProgrammatori
Naturalmente lo sviluppo non è certamente terminato, la nostra classe supporta i metodi richiesti dall'interfaccia base ICollection, ma non ha ancora alcuna funzionalità che ci permetta di riempirla, oppure di leggerne il contenuto oppure di svuotarla. Per fare tutto questo c'è ancora un po' di codice da scrivere:

  public Programmatore this[int pIndex]
  {
    get
    {
      if( pIndex < 0 || pIndex > this.Count )
      {
        throw new ApplicationException( "Indice non valido" );
      }
      return (Programmatore)this.mTeam[index];
    }
  }   

Questa è il primo metodo, probabilmente il più usato, la sua definizione è Indexer (indicizzatore) e ci fornisce la possibilità di accedere ai membri della collezione utilizzando appunto un indice:

  Programmatore p = MyTeam[21];    

Se MyTeam è una variabile di tipo TeamProgrammatori, l'indexer mi permette di accedere al 21° programmatore in questo modo.

  public void Add(Programmatore pProgrammatore)
  {
    this.mTeam.Add(pProgrammatore);
  }

Il metodo Add mi permette di aggiungere un nuovo programmatore.

  public void AddRange(Programmatore[] pProgrammatori)
  {
    this.mTeam.AddRange(pProgrammatori);
  }

Il metodo AddRange mi permette di aggiungere un array di programmatori (una lista).

  public void Sort()
  {
    this.mTeam.Sort(new TeamComparer());
  }
 
  private int Find(Programmatore pProgrammatore)
  {
    return this.mTeam.BinarySearch(pProgrammatore, new TeamComparer());
  }  

Questi due metodi, che permettono l'uno di ordinare la collection secondo il cognome dei programmatori e l'altro di trovare un programmatore nella collection, necessitano della creazione di una classe accessoria, la classe TeamComparer. Questa classe molto semplice implementerà l'interfaccia IComparer, un'interfaccia composta da un solo membro: il metodo Compare, il cui scopo è fornire un modo di comparare fra di loro due oggetti ottenendo, semplicemente, un valore che informi sul loro rapporto (il primo è minore, maggiore, uguale rispetto al secondo).
Aggiungiamo quindi in Visual Studio una classe, chiamiamola TeamComparer; nella classe base prodotta da Visual Studio andiamo a indicare il Namespace e nelle clausole using a inizio file aggiungiamo System.Collections.
A questo punto, indichiamo che la classe implementa l'interfaccia IComparer inserendo i duepunti e il nome accanto alla dichiarazione della classe, premiamo TAB come richiesto affinché vengano implementati i membri base di IComparer:

  public class TeamComparer : IComparer
  {
    public TeamComparer()
    {
      //In questo caso non ci serve inizializzare nulla
    }


    public int Compare(object pProgrammatoreX, object pProgrammatoreY )
    {
      return((new CaseInsensitiveComparer()).Compare(((Programmatore)pProgrammatoreX).Cognome,
                                                     ((Programmatore)pProgrammatoreY).Cognome));
    }
  }

Come potremo osservare, verrà implementato un metodo Compare base vuoto che modificheremo a nostro uso; la comparazione che faremo sarà effettuata sul campo cognome del programmatore, usando un oggetto CaseInsensitiveComparer; non è una comparazione precisa, perché se avessimo due programmatori con lo stesso cognome otterremmo un risultato strano sulla ricerca, ma ai fini della semplice dimostrazione non ci addentreremo di più; in seguito, volendo, sarà possibile modificare questa funzione per effettuare due comparazioni, su nome e cognome, ed ottenere quindi un dato più preciso, oppure tre, su nome cognome e data di nascita, per ottenere un dato ancor più preciso.
Grazie a questa classe, le due funzioni di Sort e Find saranno in grado di ordinare per cognome e di trovare un programmatore all'interno della nostra collezione.

  public void Remove(Programmatore pProgrammatore)
  {
    int ndx = this.Find( pProgrammatore );
    if(ndx >= 0)
    {
      this.mTeam.RemoveAt(ndx);
    }
    else
    {
      throw new ApplicationException("Il programmatore non appartiene al team");
    }
  }

Il metodo Remove, che permette di eliminare un programmatore, ci dà modo di utilizzare il metodo Find precedentemente implementato.
Il metodo Clear azzera il contenuto della collezione:

  public void Clear()
  {
    this.mTeam.Clear();
  }   

Avrete notato che i vari metodi non fanno altro che usare i metodi 'nativi' degli oggetti impiegati.

La Serializzazione di una classe
Terminate le funzioni di base, estendiamo ancora la nostra classe introducendo il concetto di Serializzazione.
Anche questa è una parola che può suonare strana a chi non ha mai lavorato con .NET, alziamo la mano per primi noi. Per comprendere il suo significato potrebbe aiutarci il fatto che il verbo serializzare e la parola serializzazione sono ispirati all'interfaccia seriale, la cara vecchia rs232: cosa fa una interfaccia seriale? prende un mucchio di bit, e li trasmette da un'altra parte uno dopo l'altro usando una determinata codifica, affinché dalla parte opposta, un'altra seriale li riceva e li decodifichi riassemblandoli nel mucchio di partenza.
La Serializzazione .NET parte dallo stesso principio, e spedisce i bit che compongono il nostro oggetto non ad un altro computer, per lo meno non direttamente, ma ad un file su disco, mentre la tecnologia complementare, la Deserializzazione, legge questo file dal disco e lo trasforma in un oggetto.

Il Framework .NET ci fornisce il necessario a creare le nostre funzioni di serializzazione grazie a due dei suoi namespace, il namespace XML che contiene tutte le funzionalità per lavorare con i dati in formato XML e il namespace Xml.Serialization che ci fornisce gli oggetti necessari alla creazione delle funzioni di serializzazione.
Perché troviamo le funzioni di serializzazione entro il namespace XML? Perché un oggetto solitamente viene serializzato per essere trasmesso da un sistema ad un altro oppure per salvarne il contenuto, e negli ultimi anni, il formato XML per la codifica dei dati e la loro trasmissione fra sistemi remoti anche con sistemi operativi diversi è quello più diffuso in quanto semplice da generare e facilmente "malleabile" tramite le funzionalità di trasformazione di cui è dotato.

Includiamo i due namespace tra le clausole using della classe TeamProgrammatori e scriviamo le funzioni di serializzazione e deserializzazione:

  public void WriteXml( string pXmlPath )
  {             
    XmlSerializer xs = new XmlSerializer (GetType());
    XmlTextWriter xw = new XmlTextWriter(pXmlPath, Encoding.UTF8 );
  
    try 
    {
      xs.Serialize( xw, this );
    }
    catch (Exception ex)
    {
      throw new ApplicationException("Teamprogrammatori.WriteXml:" + ex.Message, ex);
    }
    finally
    {
      xw.Flush();
      xw.Close();
    }
  }

La funzione di serializzazione richiede come parametro il path completo di un file dove memorizzare i dati, utilizza due oggetti standard delle classi del Framework per serializzare il nostro oggetto, ovvero un XmlSerializer e un XmlTextWriter. Il risultato prodotto dalla serializzazione sarà un file xml come questo:

Lo stesso file, potrà essere trasformato in un oggetto TeamProgrammatori deserializzandolo con la funzione seguente:

  public void ReadXml( string pXmlPath )
  {
    XmlSerializer xs = new XmlSerializer(GetType());
    XmlTextReader xr = new XmlTextReader( pXmlPath );
  
    try
    {
      this.Clear();
      mTeam.AddRange((TeamProgrammatori)xs.Deserialize(xr));
    }
    catch(Exception ex)
    {
      throw new ApplicationException("TeamProgrammatori.ReadXml: " + ex.Message, ex);
    }
    finally
    {
      xr.Close();
    }
  }   

In questo caso, utilizziamo sempre l'oggetto XmlSerializer e l'oggetto XmlTextReader per ottenere la nostra classe TeamProgrammatori dal file XML su cui è stata memorizzata.

Il Test della classe TeamProgrammatori
Per testare la nostra classe, creiamo una form che chiameremo frmTeam all'interno del progetto. Modifichiamo FrmMenu facendo doppio click sul menuItem mnuTeam e aggiungiamo il codice per aprire la form:

  private void mnuTeam_Click(object sender, System.EventArgs e)
  {
    frmTeam frm = new frmTeam();
    frm.MdiParent = this;
    frm.Show();
  }

All'interno della form, inseriamo i campi necessari ad aggiungere i programmatori al team, volendo con un copia incolla dalla form di test Programmatore. Inseriamo inoltre una listbox per elencare i programmatori che sono stati inseriti nella collezione e una textbox multilinea in cui visualizzare i dati relativi al programmatore selezionato sulla listbox. Aggiungiamo inoltre una serie di bottoni che ci permetteranno di aggiungere o togliere un programmatore, svuotare la collezione, serializzarla su disco o deserializzarla. Per selezionare il luogo e il nome del file che ospiterà i dati serializzati utilizzeremo un controllo OpenFileDialog e un controllo SaveFileDialog, che aggiungeremo alla form assieme agli altri controlli dando loro un nome opportuno. La form risultante sarà la seguente:

Vediamo cosa abbiamo implementato nel codice della nostra form; innanzitutto, abbiamo definito la nostra variabile TeamProgrammatori a livello di Form:

  public class frmTeam : System.Windows.Forms.Form
  {
    private TeamProgrammatori mTeam;    

Nel costruttore della form, instanziamo la nostra classe e carichiamo la combo box degli skill:

  public frmTeam()
  {
    InitializeComponent();
    this.mTeam = new TeamProgrammatori();
    foreach(int i in Enum.GetValues(typeof(Skills)))
    {
      this.cboSkill.Items.Add( Enum.GetName(typeof(Skills), i ) );
    }   
  }   

Copiamo le funzioni di aggiornamento della lista linguaggi per la costruzione del programmatore dalla form di test della classe programmatore:

  private void btnAddLang_Click(object sender, System.EventArgs e)
  {
    if( this.txtAddLang.Text.Length > 0 )
    {
      this.lbLinguaggi.Items.Add( this.txtAddLang.Text );
      this.txtAddLang.Text = "";
    }
    else
    {
      MessageBox.Show( "Inserire il nome di un linguaggio nella Textbox qui sopra grazie!");
    }   
  }

  private void btnClearLang_Click(object sender, System.EventArgs e)
  {
    this.lbLinguaggi.Items.Clear();
  }   

Scriviamo ora la funzione di aggiunta di un programmatore al Team:

  private void btnAggiungiPrg_Click(object sender, System.EventArgs e)
  {
    Programmatore prg = new Programmatore();
    prg.Nome = this.txtNome.Text;
    prg.Cognome = this.txtCognome.Text;
    prg.Indirizzo = this.txtIndirizzo.Text;
    prg.Citta = this.txtCitta.Text;
    prg.DataNascita = this.txtDataNascita.Text;
    prg.Skill = (Skills)this.cboSkill.SelectedIndex;
    prg.SqlKnowledge = this.chkSqlKnowledge.Checked;
    prg.BeginDate = DateTime.Parse( this.txtBeginDate.Text);
    prg.ClearLinguaggi();
    for( int i=0; i< this.lbLinguaggi.Items.Count; i++ )
    {
      prg.AggiungiLinguaggio( (string)this.lbLinguaggi.Items[i] );
    }
    this.mTeam.Add(prg);
    AggiornaLista();
    ClearControls();
  }   

In fondo alla funzione, chiamiamo due funzioni accessorie, che ci serviranno anche per altre funzioni, AggiornaLista ci serve ad aggiornare la listbox dei programmatori del team, ClearControls ad azzerare i campi dell'inserimento dati dopo aver aggiunto il programmatore.

  private void ClearControls()
  {
    this.txtNome.Text = "";
    this.txtCognome.Text = "";
    this.txtIndirizzo.Text = "";
    this.txtCitta.Text = "";
    this.txtDataNascita.Text = "";
    this.cboSkill.SelectedIndex = 0;
    this.chkSqlKnowledge.Checked = false;
    this.txtBeginDate.Text = "";
    this.lbLinguaggi.Items.Clear();
    this.txtAddLang.Text = "";
  }

  private void AggiornaLista()
  {
    this.lbTeam.Items.Clear();
    for(int i=0; i<this.mTeam.Count; i++ )
    {
      StringBuilder sb = new StringBuilder();
      sb.AppendFormat( "{0} {1}", this.mTeam[i].Cognome, this.mTeam[i].Nome );
      this.lbTeam.Items.Add( sb.ToString() );
    }
    this.lbTeam.SelectedIndex = this.mTeam.Count-1;
  }

Aggiungiamo alla listbox una funzione sulla gestione del SelectedIndexChanged per visualizzare i dati del programmatore selezionato sulla textbox preposta allo scopo:

  private void lbTeam_SelectedIndexChanged(object sender, System.EventArgs e)
  {
    AggiornaLabel();
  }   

E la funzione AggiornaLabel che potrà essere usata in più punti e creiamo separatamente:

  private void AggiornaLabel()
  {
    this.txtPrgSel.Text = this.mTeam[this.lbTeam.SelectedIndex].ToString();
  }

Ora implementiamo il codice per i vari pulsanti. La funzione che cancella il programmatore selezionato sulla listbox:

  private void btnEliminaPrg_Click(object sender, System.EventArgs e)
  {
    if( lbTeam.SelectedIndex > -1 )
    {
      if( MessageBox.Show( "Confermi la cancellazione di: "+
                           Environment.NewLine + this.mTeam[lbTeam.SelectedIndex].ToString(),
                           "Conferma cancellazione", MessageBoxButtons.YesNo, 
                           MessageBoxIcon.Question
                         ) == DialogResult.Yes )
      {
        this.mTeam.Remove( this.mTeam[lbTeam.SelectedIndex] );
        AggiornaLista();
      }
    }
    else
    {
      MessageBox.Show( "Nessun Programmatore selezionato, selezionarne uno dalla lista.", 
                       "Attenzione!" );
    }
  }

La funzione di svuotamento della collezione:

  private void btnClearPrg_Click(object sender, System.EventArgs e)
  {
    this.mTeam.Clear();
    ClearControls();
    this.lbTeam.Items.Clear();
    this.txtPrgSel.Text = "";
  }

La serializzazione su XML, per la quale utilizziamo il controllo SaveFileDialog che abbiamo chiamato sfdXml e predisposto per usare la cartella c:\temp e il nome file standard team01.xml, che comunque potrà essere modificato dall'utente.

  private void btnSalvaXml_Click(object sender, System.EventArgs e)
  {
    if(  sfdXml.ShowDialog() == DialogResult.OK )
    {
      mTeam.WriteXml( sfdXml.FileName );        
    }
  } 

La deserializzazione da XML, per la quale utilizziamo il controllo OpenFileDialog che abbiamo chiamato ofdXml e predisposto per usare la stessa cartella e lo stesso file della dialog di salvataggio, naturalmente modificabile a piacere.

  private void btnCaricaXml_Click(object sender, System.EventArgs e)
  {
    if( ofdXml.ShowDialog() == DialogResult.OK )
    {
      mTeam.ReadXml( ofdXml.FileName );
      AggiornaLista();
      AggiornaLabel();
    }
  }   

A questo punto abbiamo un progetto di test su cui provare le funzioni base della nostra classe TeamProgrammatori, facendo un po' di prove, il risultato visibile può essere simile a questo:

Aggiungiamo ancora un pulsante per testare la funzione Sort che ci permette di ordinare i programmatori per cognome:

  private void btnSort_Click(object sender, System.EventArgs e)
  {
    this.mTeam.Sort();
    AggiornaLista();
  }   

Otterremo qualcosa di simile a questo:

E a questo punto, se non siete ancora crollati per lo sfinimento da lettura, scaricate il progetto di test e fate i vostri esperimenti e le vostre modifiche, noi iniziamo a pensare a qualcosa di nuovo per portarvi ancor più addentro alla OOP.

Precedente