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:
- eliminate il file AppConfiguration.vb;
- rinominate IniSettingStudioUserConfig (il file, la classe, e tutti i punti in cui viene usato) in IniSettingsStudioUserConfig (mi è sfuggita una esse);
- nelle proprietà del progetto, scheda Compile, impostate Option Strict a On (questa è stata una grave dimenticanza da parte mia) e correggete i due soli errori: nel codice della sub Cancel_Button_Click della classe DlgUser, sostituite le prime due righe con le seguenti:
Me.txtNickName.Text = Me.txtNickName.Tag.ToString Me.txtEmail.Text = Me.txtEmail.Tag.ToStringAggiunta 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 ObjectQuindi 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 FunctionPer 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 objNaturalmente, 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 PropertyAdesso 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 InterfaceLa 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 ClassAdesso 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 ClassPublic 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 ClassNotate 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 ClassPoi 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.