Impostazioni di applicazione in Visual Basic Net (3)
a cura di Diego Cattaruzza (requisiti: Framework 2.0)

Premessa
Questo articolo è il terzo di una serie. Nel primo è stata illustrata la gestione di impostazioni 'comoda', dipendente dal designer di Visual Studio. Nel secondo è stato iniziato lo sviluppo di una gestione 'propria', un po' meno comoda, ma più controllabile, studiando la creazione di alcune classi, prima applicando l'ereditarietà e poi il polimorfismo, con una interfaccia. Di conseguenza si invita a leggere le puntate precedenti e soprattutto a scaricare almeno il codice a corredo del secondo articolo, poiché è da quel punto che si riprende lo sviluppo illustrato in questo.

Poiché il codice fornito a corredo di questo articolo è solo il risultato finale dello studio, si consiglia di seguire lo svolgimento dell'articolo, apportando via via le modifiche al codice che verranno indicate.

Correzione di imperfezioni e distrazioni
Concentrato sulla stesura dell'articolo, ho commesso degli errori nello sviluppare il progetto di studio. Prima di proseguire, effettuate i seguenti cambiamenti:

Aggiunta di metodi all'interfaccia IConfigurazione
Le classi che si stanno sviluppando devono esporre un metodo per scrivere le proprietà (Save) e uno per leggerle (Load).
Di questi metodi bisogna inserire la firma nell'interfaccia. Per quanto riguarda il primo metodo, non c'è alcun problema, ma per il secondo bisogna pensarci un po', perché deve restituire un oggetto dello stesso tipo da cui è chiamato, e una interfaccia non può conoscere l'esistenza delle classi da sé derivate - il fatto che lo sappiamo noi, quali sono, non ci esime dal cercare di fare una programmazione corretta - di conseguenza si restituirà un tipo Object.
Aggiungete quindi all'interfaccia le firme dei metodi:

  Sub Save()

  Function Load() As Object

Quindi sviluppate le due classi derivate aggiungendo i metodi dovuti:

  Public Sub Save() Implements IConfigurazione.Save

  End Sub

  Public Function Load() As Object Implements IConfigurazione.Load

  End Function

Per scrivere un file xml (come sono i file di configurazione), abbiamo bisogno di un serializzatore che legga le caratteristiche dell'oggetto che gli passiamo e le scriva in formato xml attraverso uno scrittore di file impostato sul file di configurazione che vogliamo scrivere.
Ecco quindi il codice da implementare nel metodo Save della classe IniSettingsStudioConfig:

    Dim serializer As New Xml.Serialization.XmlSerializer(GetType(IniSettingsStudioConfig))
    Dim writer As New IO.StreamWriter(ConfigFileName)
    serializer.Serialize(writer, Me)
    writer.Close()

Per la classe IniSettingsStudioUserConfig, basta sostituire il tipo su cui impostare il serializzatore (nella prima riga).
Per il metodo Load, il sistema è quasi analogo: si usa un lettore di file e del serializzatore viene usato il metodo Deserialize che restituisce un oggetto del tipo richiesto. Si riporta solo il metodo per la classe IniSettingsStudioConfig, per la classe IniSettingsStudioUserConfig ci sono tre ricorrenze IniSettingsStudioConfig da sostituire:

    Dim serializer As New Xml.Serialization.XmlSerializer(GetType(IniSettingsStudioConfig))
    Dim reader As New IO.StreamReader(ConfigFileName)
    Dim obj As IniSettingsStudioConfig = DirectCast(serializer.Deserialize(reader), _
                                                    IniSettingsStudioConfig)
    reader.Close()
    Return obj

Naturalmente, se usati adesso, questi metodi scriverebbero o leggerebbero dei file vuoti, dato che le uniche proprietà esposte sono di sola lettura. L'idea è proprio quella di limitarsi ad aggiungere le proprietà che interessano.
Aggiungete quindi, alla classe IniSettingsStudioUserConfig, le proprietà corrispondenti alle due impostazioni utente cui è associata la form di dialogo DlgUser, impostando anche i valori di default:

  Private mNickName As String = "NoName"
  Public Property NickName() As String
    Get
      Return mNickName
    End Get
    Set(ByVal value As String)
      mNickName = value
    End Set
  End Property

  Private mEmail As String = "NoEmail"
  Public Property Email() As String
    Get
      Return mEmail
    End Get
    Set(ByVal value As String)
      mEmail = value
    End Set
  End Property

Adesso potete provare a scrivere, sostituendo il codice del metodo cmdNuovoConfig_Click con:

    Dim cr As String = Environment.NewLine
    MessageBox.Show(String.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}", _
                                  "IniUser.ConfigFileName: " & cr, IniUser.ConfigFileName, cr, _
                                  "IniUser.Scope: ", IniUser.Scope.ToString, cr, _
                                  "IniUser.NickName: ", IniUser.NickName, cr, _
                                  "IniUser.Email: ", IniUser.Email), _
                    "Nuovo Config - prima di scrivere")

Eseguite e fate clic sul pulsante "Nuovo Config"; verificate che le nuove proprietà hanno i valori di default.
Notate che non è stato letto il file: sono i valori di default della classe.
Adesso sostituite il codice appena scritto con il seguente:

    If Not My.Computer.FileSystem.FileExists(IniUser.ConfigFileName) Then
      IniUser.Save()
    End If

    IniUser = DirectCast(IniUser.Load, IniSettingsStudioUserConfig)

    Dim cr As String = Environment.NewLine
    MessageBox.Show(String.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}", _
                                  "IniUser.ConfigFileName: " & cr, IniUser.ConfigFileName, cr, _
                                  "IniUser.Scope: ", IniUser.Scope.ToString, cr, _
                                  "IniUser.NickName: ", IniUser.NickName, cr, _
                                  "IniUser.Email: ", IniUser.Email), _
                    "Nuovo Config")

    IniUser.NickName = "NuovoNickName"
    IniUser.Save()

La procedura inizia con il controllare l'esistenza del file; se non esiste lo crea salvando le impostazioni (che la prima volta sono quelle di default). Quindi legge le impostazioni - effettuando, si noti, la tipizzazione dell'Object restituito dal metodo - e le presenta. Poi cambia la proprietà NickName e salva di nuovo.
Eseguite e fate clic sul pulsante "Nuovo Config". Chiudete e ri-eseguite; verificate che la seconda volta la proprietà NickName ha il valore nuovo.
Infatti, se nel metodo cmdViewUserConfig_Click, modificate il percorso passato alla form DlgFileView:

    Dim df As New DlgFileView(IniUser.ConfigFileName)

potrete verificare che contiene:

<?xml version="1.0" encoding="utf-8"?>
<IniSettingsStudioUserConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                             xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <NickName>NuovoNickName1</NickName>
  <Email>NoEmail</Email>
</IniSettingsStudioUserConfig>

Allo stesso modo si può aggiungere una proprietà, ad esempio ConnectionString, alla classe IniSettingsStudioConfig e usare la sua istanza IniApp.

Analisi dei difetti
La struttura così progettata funziona, ma non è accettabile. E' stata sfruttata la programmazione a oggetti (usando un'interfaccia e creando due classi che la implementano), ma l'interfaccia presenta un metodo che restituisce un tipo Object, il che costringe a una sua tipizzazione successiva, e le classi non restano immutabili, cioè il codice non è davvero riusabile: in un'altra applicazione potremmo voler aggiungere altre proprietà o alcune di quelle presenti potrebbero essere del tutto inutili.

In altre parole, questa struttura va bene solo una volta, il che, a parte il fatto che si ha imparato qualcosa, non va affatto bene. Bisogna ripensare tutto.

Un tipo generico
Ci vuole una struttura che permetta di creare una (o due) classi per ciascuna applicazione, che erediti (ereditino) da una (o due) classi base immutabili.
Ma non si conosce a priori il tipo (cioè, non si conosce a priori il nome della classe derivata, che avrà evidentemente un riferimento al nome dell'applicazione).
Ci vuole un tipo generico, che non sia Object.

Il .Net Framework 2.0 ci fornisce una simbologia proprio per necessità come questa. Modificate l'interfaccia come segue:

Interface IConfigurazione(Of T)

  ReadOnly Property Scope() As ConfigurationScope

  ReadOnly Property ConfigFileName() As String

  Sub Save()

  Function Load() As T

End Interface

La T rappresenta un tipo generico, che sta al programmatore, al momento dell'uso, precisare. Con questa impostazione, il metodo Load restituirà un oggetto dello stesso tipo della specifica classe che espone il metodo e non più un Object (non sarà più necessario tipizzare l'Object restituito e soprattutto non si dovrà sostituire il nome del tipo in tutte le sue ricorrenze).

A questo punto si possono creare due classi generiche di base, da cui si potranno derivare le specifiche classi per ciascuna applicazione che ne avesse necessità.
Nello stesso file IConfigurazione.vb, aggiungete le due classi generiche ConfigurazioneBaseApplicazione e ConfigurazioneBaseUtente, riprendendo e modificando il codice di IniSettingsStudioConfig e IniSettingsStudioUserConfig (si mostra solo la prima, dato che l'altra è praticamente uguale, differendo soltanto per le due proprietà di sola lettura):

Public Class ConfigurazioneBaseApplicazione(Of T)
  Implements IConfigurazione(Of T)

  Public ReadOnly Property ConfigFileName() As String Implements IConfigurazione(Of T).ConfigFileName
    Get
      Return Application.CommonAppDataPath & "\" & Application.ProductName & ".config"
    End Get
  End Property

  Public Function Load() As T Implements IConfigurazione(Of T).Load
    Dim serializer As New Xml.Serialization.XmlSerializer(GetType(T))
    Dim reader As New IO.StreamReader(ConfigFileName)
    Dim obj As T = DirectCast(serializer.Deserialize(reader), T)
    reader.Close()
    Return obj
  End Function

  Public Sub Save() Implements IConfigurazione(Of T).Save
    Dim serializer As New Xml.Serialization.XmlSerializer(GetType(T))
    Dim writer As New IO.StreamWriter(ConfigFileName)
    serializer.Serialize(writer, Me)
    writer.Close()
  End Sub

  Public ReadOnly Property Scope() As ConfigurationScope Implements IConfigurazione(Of T).Scope
    Get
      Return ConfigurationScope.ApplicationScope
    End Get
  End Property
End Class

Adesso sì che si è sviluppato un sistema davvero riusabile per ciascuna applicazione. Infatti adesso potete modificare IniSettingsStudioConfig e IniSettingsStudioUserConfig senza dover più toccare le classi base:

Public Class IniSettingsStudioConfig
  Inherits ConfigurazioneBaseApplicazione(Of IniSettingsStudioConfig)

  Private mConnectionString As String
  Public Property ConnectionString() As String
    Get
      Return mConnectionString
    End Get
    Set(ByVal value As String)
      mConnectionString = value
    End Set
  End Property

End Class
Public Class IniSettingsStudioUserConfig
  Inherits ConfigurazioneBaseUtente(Of IniSettingsStudioUserConfig)

  Private mNickName As String = "NoName"
  Public Property NickName() As String
    Get
      Return mNickName
    End Get
    Set(ByVal value As String)
      mNickName = value
    End Set
  End Property

  Private mEmail As String = "NoEmail"
  Public Property Email() As String
    Get
      Return mEmail
    End Get
    Set(ByVal value As String)
      mEmail = value
    End Set
  End Property

End Class

Notate che non dovete toccare alcuna parte del codice che usa le classi IniSettingsStudio..., e che queste espongono solo le proprietà specificamente necessarie per l'applicazione per la quale sono state create.
Anzi, c'è una riga da cambiare nel metodo cmdNuovoConfig_Click, dato che non occorre più tipizzare un oggetto Object:

    IniUser = IniUser.Load()

L'obiettivo più importante è che ogni classe specifica per la configurazione delle nostre prossime applicazioni non richiederà altro che la dichiarazione di eredità (nell'esempio successivo, si suppone una applicazione di nome PincoPallino):

Public Class PincoPallinoConfig
  Inherits ConfigurazioneBaseApplicazione(Of PincoPallinoConfig)

End Class

Poi non resta che aggiungere le proprietà corrispondenti alle impostazioni che si vogliono persistere.

Se tornaste alla situazione precedente, e provaste a ereditare IniSettingsStudioConfig o IniSettingsStudioUserConfig (cui potreste anche cambiare nome), vedreste che sarebbe comunque necessario andare a sostituire le ricorrenze del tipo nei metodi Load e Save. Con la classe Generic (così si chiama il tipo di classi che avete appena sviluppato) è stata costruita una struttura di interfaccia, tipo enumerato e classi assolutamente completa e robusta.

Conclusioni
In questo articolo si è terminato lo sviluppo di un sistema personalizzato di gestione delle impostazioni di programma, ne è stata constatata l'inefficienza, ne è stata implementata una sostituzione efficiente, facendo uso di un tipo generico.
Potrebbe forse bastare, ma pare invece opportuno affrontare il tema anche da qualche altro punto di vista, come si cercherà di fare in un prossimo articolo.
Il codice a corredo di questo articolo è scaricabile dall'area download.