Configurare un'applicazione
a cura di Rudy Azzan (requisiti: conoscenza generica di .Net)

Introduzione
Molte sono le novità introdotte in .NET 2.0 riguardo alla configurazione delle applicazioni e molta è la confusione generata da esse. Per questo proverò a fare un po' di luce sugli arcani meccanismi nascosti che i programmatori Microsoft ci hanno messo a disposizione.
Il mio obbiettivo non è quello di spiegare per filo e per segno come funzionano i NameSpace di configurazione, bensì analizzare degli aspetti apparentemente ovvi, ma che in realtà non lo sono.

Iniziamo
Prima di tutto vediamo cosa Visual Studio ci offre per gestire i Setting di default.
Apriamo o creiamo un nuovo progetto Visual Studio. Andiamo a vedere le proprietà del progetto e selezioniamo la tab "Settings". Qui troviamo una maschera che ci permette di inserire nuovi valori.
Nella prima colonna scriviamo il nome univoco del setting, che poi useremo per farvi riferimento nel codice.
Nella seconda colonna specifichiamo il tipo del valore che inseriremo.
La terza colonna specifica lo scope del setting che può essere User o Application. Un setting con scope User vale per l'utente che ha avviato l'applicazione, ciò significa che se siete entrati con l'utente "Mario" e avete modificato un setting a runtime, per l'utente "Luigi" questo setting è rimasto quello di default. Questo succede perché, specificando voi lo scope di tipo User, il Framework crea un file (vedremo dopo come, dove e perché) chiamato "user.config", in cui salva i setting che noi modifichiamo per l'utente, quindi ognuno avrà il suo file e i suoi setting. Specificando lo scope Application, invece, il setting vale per tutti gli utenti, quindi, se cambia per uno, cambia per tutti. E' importante sapere che per i setting utente il file nomeapplicazione.exe.config contiene i valori di default e, fino a quando questi valori non vengono modificati, vengono presi da questo file.
Infine, l'ultima colonna "Value" specifica il valore di default che avrà il setting.

Ora cominciamo a fare qualche esperimento che ci chiarirà le idee. Prima di tutto bisogna referenziare dalla GAC la dll System.Configuration.

Le due classi che useremo principalmente si chiamano Configuration e ConfigurationManager.
Il file di setting nomeapplicazione.exe.config è un file xml composto da varie parti chiamate sectionGroups che a loro volta contengono sections.
Ecco una lista di alcuni membri che vedremo in dettaglio più avanti:

Configuration  
AppSettings restituisce l'oggetto AppSettingsSection
ConnectionStrings restituisce l'oggetto ConnectionStringsSection
EvaluationContext restituisce l'oggetto ContextInformation per i dettagli sull'environment legato all'elemento della configurazione
FilePath restituisce il percorso fisico del file di configurazione
GetSection restituisce uno specifico oggetto Configuration-Section
GetSectionGroup restituisce uno specifico oggetto Configuration-SectionGroup
HasFile indica se esiste un file di configurazione per la risorsa rappresentata
NamespaceDeclared restituisce o imposta un valore che indica se nel file di configurazione è stato dichiarato un NameSpace XML
RootSectionGroup restituisce l'oggetto ConfigurationSectionGroup principale
Save scrive i setting del configuration object nel file xml di configurazione corrente
SaveAs scrive i setting del configuration object nel file xml di configurazione specificato
ConfigurationManager (metodi statici)
AppSettings restituisce l'oggetto NameValueCollection dell'oggetto AppSettingsSection della configurazione dell'applicazione corrente
ConnectionStrings restituisce l'oggetto ConnectionStringsCollection della ConnectionStringsSection della configurazione dell'applicazione corrente
GetSection restituisce una specifica Configuration-Section della configurazione dell'applicazione corrente
OpenExeConfiguration apre la configurazione del client specificata e restituisce un oggetto Configuration
OpenMachine-Configuration apre il file Machine.config del computer corrente e restituisce un oggetto Configuration
OpenMappedExe-Configuration apre la configurazione del client specificata e restituisce un oggetto Configuration usando il file mapping e il livello utente specificati
OpenMappedMachine-Configuration apre il file Machine.config del computer corrente e restituisce un oggetto Configuration usando il file mapping e il livello utente specificati

Il primo passo che compiremo consiste nel capire come aprire i file di gestione della configurazione.

ConfigurationManager ci mette a disposizione il metodo OpenExeConfiguration. Questo metodo ha due overload. Si può passare il percorso completo del file exe oppure uno dei tre valori dell'enumerato ConfigurationUserLevel:

Mentre il primo parametro è chiaro per tutti, per capire meglio gli altri due dovremmo sapere cosa si intende per roaming. A grandi linee dirò che per roaming si intende la possibilità di portare automaticamente la configurazione di un utente da un computer ad un altro. Se siete in una rete in cui il vostro amministratore di sistema vi ha abilitato il roaming, quando farete il login su un qualsiasi computer i vostri setting saranno recuperati e salvati su questo computer.
Con PerUserRoaming intendiamo il file user.config situato in "\Documents and Settings\[Nome utente]\Dati applicazioni\[ AssemblyCompany]\[Nome applicazione]\[ AssemblyVersion]\user.config" che rappresenta i setting dell'utente in un contesto di roaming.
Con PerUserRoaming-AndLocal intendiamo il file user.config situato in "\Documents and Settings\[Nome utente]\Impostazioni locali\Dati applicazioni\[AssemblyCompany]\[Nome applicazione]\[AssemblyVersion]\user.config" che rappresenta i setting dell'utente sul computer locale.
Fate attenzione che questi files verranno creati solamente se avete modificato i setting utente dall'applicazione con le apposite classi.

Il metodo OpenMappedExe-Configuration è simile all'altro metodo, solo che specifichiamo noi via codice dove il runtime può trovare i file di configurazione.

Esperimento
Ora che abbiamo capito come funzionano i setting nel contesto utente, facciamo un piccolo esperimento divertente nel contesto applicazione:
Creiamo un nuovo progetto di tipo Windows Application e chiamiamolo "ConfigProveVB" o "ConfigProveCSharp". Apriamo le proprietà del progetto, andiamo nella sezione Settings e inseriamo Nome: "MYSET1" Tipo: "Stringa" Scope: "Application" Valore: "Mio valore".
All'evento load del form eseguiamo le seguenti operazioni:

Apriamo il file di configurazione dell'applicazione (nomeapplicazione.exe.config).

  ' VB
  Dim config As Configuration
  config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None)
  // C#
  Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

Troviamo la sezione che ci interessa modificare.

  ' VB
  Dim grpApp As ConfigurationSectionGroup
  grpApp = config.GetSectionGroup("applicationSettings")
  Dim secPropSet As ClientSettingsSection
  secPropSet = CType(grpApp.Sections("ConfigProve.Properties.Settings"), ClientSettingsSection)
  // C#
  ConfigurationSectionGroup grpApp = config.GetSectionGroup("applicationSettings");
  ClientSettingsSection secPropSet =
    (ClientSettingsSection)grpApp.Sections["ConfigProve.Properties.Settings"];

Informiamo il runtime che, se modifichiamo questa section, deve essere sovrascritto il valore di default. Questo bisogna specificarlo perché altrimenti il runtime non considera la section modificata, quando chiamiamo il metodo Save.

  ' VB
  secPropSet.SectionInformation.ForceSave = True
  // C#
  secPropSet.SectionInformation.ForceSave = true;

Cambiamo il valore.

  ' VB
  Dim elm As SettingElement
  elm = secPropSet.Settings.Get("MYSET1")
  elm.Value.ValueXml.InnerText = "Nuovo valore"
  // C#
  SettingElement elm = secPropSet.Settings.Get("MYSET1");
  elm.Value.ValueXml.InnerText = "Nuovo valore";

Salviamo il file con la nuova configurazione.

  ' VB
  config.Save()
  // C#
  config.Save();

Perfetto, ora ci aspettiamo di vedere il file di configurazione con il valore cambiato. Apriamo il file nomeapplicazione.exe.config e... sorpresa! Il file non è cambiato.
Ora, come me, la maggior parte dei programmatori pensano: "Ho sbagliato qualcosa, non funziona, era meglio il VB 6 ecc...". Niente paura: non avete sbagliato niente!

Con l'introduzione di Visual Studio 2005, infatti, c'è una novità nel debug chiamata "hosting process" e questo è il nostro tallone d'Achille. L'hosting process, infatti, è stato introdotto per migliorare le prestazioni in fase di debug ed essere sicuri che la vostra applicazione non giri direttamente. Questo cosa significa? Significa che, se lanciate la vostra applicazione da Visual Studio 2005, sia in debug che non, esso crea una copia del vero eseguibile, la chiama nomeapplicazione.vshost.exe, la esegue e lavora con essa!!!

Questa novità comporta una importante conseguenza per noi modificatori di file di configurazione: infatti, se viene eseguito

  Configuration config = ConfigurationManager.OpenExeConfiguration (ConfigurationUserLevel.None)

da Visual Studio 2005 lavoriamo sul file di configurazione nomeapplicazione.vshost.exe.config. E, allora, perché anche il file nomeapplicazione.vshost.exe.config non presenta il nostro setting modificato? E' semplice: quando il processo che usava il file di configurazione si interrompe, Visual Studio 2005 prende il file nomeapplicazione.exe.config e lo copia sopra a nomeapplicazione.vshost.exe.config.

Se eseguite l'applicazione sopra citata e, prima di chiudere la form, andate a vedere il file nomeapplicazione.vshost.exe.config, noterete che contiene il nostro setting modificato. Se, invece di eseguire il programma da Visual Studio, lanciate l'eseguibile ("ConfigProveVB.exe") da Gestione Risorse, noterete con piacere che il file di configurazione cambia e rimane modificato.

Una soluzione per risolvere il problema è disabilitare l'hosting process (con tutti gli svantaggi che comporta).
Per fare ciò si va in proprietà del progetto nella sezione "Debug" e si disabilita la voce "Enable the Visual Studio hosting process". Cosa che io non farei, ma dipende tutto dalle vostre esigenze. Se, ad esempio, voleste fare un'applicazione per cambiare gli application settings che funzioni in tutti e due i casi, potreste chiamare l'OpenExeConfiguration sul file di configurazione nomeapplicazione.exe.config, essendo consapevoli però che il file realmente in uso è nomeapplicazione.vshost.exe.config.
Ecco un esempio:

  ' VB
  Dim appConfigVHost As String
  appConfigVHost = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _
                                          AppDomain.CurrentDomain.FriendlyName)
  Dim appConfig As String
  appConfig = appConfigVHost.Replace("vshost.", String.Empty)
  Dim config As Configuration
  config = ConfigurationManager.OpenExeConfiguration(appConfig)
  // C#
  string appConfigVHost = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
                                                 AppDomain.CurrentDomain.FriendlyName);
  string appConfig = appConfigVHost.Replace("vshost.", String.Empty);
  Configuration config = ConfigurationManager.OpenExeConfiguration(appConfig);

Alcune precauzioni
E' importante ricordare che dovete essere sicuri di avere sempre tutti i permessi necessari per poter modificare questi file di configurazione. Fate anche attenzione al fatto che, se usate il metodo ExeConfigFilename e indicate un file che non esiste, non riceverete alcun errore, ma tutti i valori che cercherete varranno null. Sta a voi implementare l'opportuna logica applicativa. Ad esempio:

  ' VB
  Dim cs As Configuration = ConfigurationManager.OpenMachineConfiguration()
  Debug.Assert(File.Exists(ExeFileName), " Percorso non valido o non trovato!");
  MyMap.ExeConfigFilename = ExeFileName
  // C#
  ExeConfigurationFileMap MyMap = new ExeConfigurationFileMap();
  String ExeFileName = @"DBConnectionString.exe.config";
  Debug.Assert(File.Exists(ExeFileName), " Percorso non valido o non trovato!");
  MyMap.ExeConfigFilename = ExeFileName;

Fate anche attenzione ai SettingElement di scope User. Se aprite il file di configurazione in modalità file user (ConfigurationUserLevel.PerUserRoamingAndLocal) e cercate un elemento in una section, se esso esiste in nomeapplicazione.exe.config ma non in user.config, viene trovato lo stesso, ma la sua proprietà ElementInformation.IsPresent vale false. In questo caso, se tentate di valorizzarlo, riceverete un errore di runtime.

ApplicationSettingsBase
Apriamo ora il file Settings.Designer (.cs o .vb) autogenerato da Visual Studio. Come possiamo notare dalle prime righe di codice:

  ' VB
  Partial Friend NotInheritable Class MySettings
        Inherits Global.System.Configuration.ApplicationSettingsBase
  // C#
  internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {

La nostra classe Settings deriva da ApplicationSettingsBase. Le proprietà esposte presentano l'attributo ApplicationScopedSettingAttribute per indicare che il setting è a livello applicazione e come possiamo notare è readonly.

  ' VB
  <Global.System.Configuration.ApplicationScopedSettingAttribute(),  _
   Global.System.Diagnostics.DebuggerNonUserCodeAttribute(),  _
   Global.System.Configuration.DefaultSettingValueAttribute("Mio valore")>  _
  Public ReadOnly Property MYSET1() As String
      Get
          Return CType(Me("MYSET1"),String)
      End Get
  End Property
  // C#
  [global::System.Configuration.ApplicationScopedSettingAttribute()]
  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
  [global::System.Configuration.DefaultSettingValueAttribute("Mio valore")]
  public string MYSET1 {
    get {
      return ((string)(this["MYSET1"]));
    }
  }

Oppure UserScopedSettingAttribute che indica un setting a livello utente modificabile.

  ' VB
  <Global.System.Configuration.UserScopedSettingAttribute(),  _
   Global.System.Diagnostics.DebuggerNonUserCodeAttribute(),  _
   Global.System.Configuration.DefaultSettingValueAttribute("Mio user valore")>  _
  Public Property MYUSERSET1() As String
      Get
          Return CType(Me("MYUSERSET1"),String)
      End Get
      Set
          Me("MYUSERSET1") = value
      End Set
  End Property
  // C#
  [global::System.Configuration.UserScopedSettingAttribute()]
  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
  [global::System.Configuration.DefaultSettingValueAttribute("Mio user valore")]
  public string MYUSERSET1 {
    get {
      return ((string)(this["MYUSERSET1"]));
    }
    set {
      this["MYUSERSET1"] = value;
    }
  }

Utilizzando quindi la classe Settings possiamo scrivere:

  ' VB
  My.Settings.MYUSERSET1 = "Nuovo valore"
  My.Settings.Save()
  // C#
  ConfigProveCSharp.Properties.Settings.Default.MYUSERSET1 = "Nuovo valore";
  ConfigProveCSharp.Properties.Settings.Default.Save();

In questo modo il nuovo valore per il setting di livello utente "MYUSERSET1" sarà valorizzato a "Nuovo valore". Non è possibile fare la stessa cosa con setting a livello applicazione in quanto sono in sola lettura, ma, come abbiamo visto prima, questo non è un problema.

Un problema lo potremmo riscontrare, invece, quando facciamo un'applicazione di setup e impostiamo nelle proprietà del progetto RemovePreviousVersions = true; questo dobbiamo impostarlo perché, se facciamo delle modifiche ad un progetto e vogliamo distribuirlo, se è già installata un'altra versione, il programma di setup non sovrascrive i vecchi file (non ho trovato altra maniera, ma, se qualcuno ha idee in proposito, può contattarmi, grazie). Sovrascrivendo i file dell'applicazione, però, viene sovrascritto pure nomeapplicazione.exe.config e ciò significa che, se lo avevamo modificato, perdiamo le modifiche.

Questo non succede però per i setting a livello User, ma, ancora, se cambiate il numero di versione dell'applicazione, essi ritorneranno ai valori di default, perché il runtime non trova più il file user.config. Fortunatamente, almeno in questo caso, il Framework ci viene in aiuto col metodo "Upgrade" di "ApplicationSettingsBase": questo metodo ripristina i valori dell'utente a quelli della versione precedentemente installata:

  ' VB
  My.Settings.Upgrade()
  // C#
  ConfigProveCSharp.Properties.Settings.Default.Upgrade();

Il suo contrario è Reset, che reimposta i valori a quelli di default.
Se non vogliamo ripristinare tutti i valori, ma solo alcuni, possiamo ottenere quelli della versione precedente col metodo GetPreviousVersion, al quale passiamo come argomento il nome del setting:

  ' VB
  My.Settings.MYUSERSET1 = My.Settings.GetPreviousVersion("MYUSERSET1")
  // C#
  ConfigProveCSharp.Properties.Settings.Default.MYUSERSET1 = (string)
  ConfigProve.Properties.Settings.Default.GetPreviousVersion("MYUSERSET1");

Una soluzione, se si modificano i setting a livello applicazione, potrebbe essere fare una copia del file nomeapplicazione.exe.config, perché la copia non viene rimossa, quando si disinstalla l'applicazione. Poi si può implementare qualche meccanismo per ripristinarla al setup successivo.

Creiamo una section personalizzata
Il file xml di setting è composto da sezioni e gruppi; un gruppo contiene una o più sezioni. Il Framework ci mette a disposizione una serie di classi e interfacce per creare le nostre classi personalizzate.
Come prima cosa creiamo una SectionGroup nel file xml dei setting all'interno di ConfigSections, specificando il tipo in maniera estesa:

  <sectionGroup name="miaSezione" type="ConfigProve.MiaSezione, ConfigProve">
    <section name="prova" type="ConfigProve.MiaSezione, ConfigProve" />
  </sectionGroup>

Successivamente creiamo i dati per la nostra sectionGroup:

  <miaSezione>
    <prova>
      <valore>
        <codice>A1</codice>
        <valoreSetting>Prova1</valoreSetting>
      </valore>
      <valore>
        <codice>B2</codice>
        <valoreSetting>Prova2</valoreSetting>
      </valore>
    </prova>
  </miaSezione>

Ora creiamo la nostra classe, indichiamo che implementa l'interfaccia IConfigurationSectionHandler e codifichiamo il metodo Create:

  ' VB
  Imports System.Configuration
  Imports System.Xml

  Public Class MiaSezione
      Implements IConfigurationSectionHandler

      Public Function Create(ByVal parent As Object, ByVal configContext As Object, ByVal section As XmlNode) As Object _
          Implements IConfigurationSectionHandler.Create

          'VB
          Dim ConfigValues As New Hashtable
          Dim Root As XmlElement = CType(section, XmlElement)
          Dim TempValue As String

          For Each ParentNode As XmlNode In Root.ChildNodes
              For Each ChildNode As XmlNode In ParentNode.ChildNodes

                  If ChildNode.Name = "Identifier" Then
                      TempValue = ChildNode.InnerText
                  End If
                  If ChildNode.Name = "SettingValue" Then
                      TempValue = ChildNode.InnerText
                  End If
              Next
          Next

          Dim MyHandler As New ValuesHandler(ConfigValues)
          Return MyHandler

      End Function

  End Class

  'VB
  Public Class ValuesHandler

      Private customValue As Hashtable

      Public Sub New(ByVal configValues As Hashtable)
          Me.customValue = configValues
      End Sub

      Public Function GetValueFromKey(ByVal key As String) As String
          Return Me.customValue(key)
      End Function

  End Class
  // C# 
  #region Using directives

  using System;
  using System.Collections.Generic;
  using System.Text;
  using System.Configuration;
  using System.Collections;
  using System.Xml;

  #endregion

  namespace ConfigProveCSharp
  {
    public class MiaSezione : IConfigurationSectionHandler
    {
      #region IConfigurationSectionHandler Members

      public object Create(object parent, object configContext, XmlNode section)
      {
        // C#
        Hashtable ConfigValues = new Hashtable();
        XmlElement Root = section as XmlElement;
        String TempValue = string.Empty;
        foreach (XmlNode ParentNode in Root.ChildNodes)
        {
          foreach (XmlNode ChildNode in ParentNode.ChildNodes)
          {
            if (ChildNode.Name == "codice")
            {
              TempValue = ChildNode.InnerText;
            }
            if (ChildNode.Name == "valoreSetting")
            {
              ConfigValues[TempValue] = ChildNode.InnerText;
            }
          }
        }
        ValuesHandler MyHandler = new ValuesHandler(ConfigValues);
        return MyHandler;
      }

      #endregion
    }

    // C#
    class ValuesHandler
    {
      private Hashtable customValue;
      public ValuesHandler(Hashtable configValues)
      {
        this.customValue = configValues;
      }
      public String GetValueFromKey(String key)
      {
        return this.customValue[key] as String;
      }
    }
  }

Ora possiamo ottenere i nostri setting nell'applicazione chiamando:

  ' VB
  Dim vals As ValuesHandler
  vals = CType(ConfigurationManager.GetSection("miaSezione/prova"), ValuesHandler)
  // C#
  ValuesHandler vals = ConfigurationManager.GetSection("miaSezione/prova") as ValuesHandler;

Conclusione
In generale, ho notato che ci sono aspetti positivi e negativi utilizzando questo tipo di approccio alla configurazione di un'applicazione; certo è che la confusione e la difficoltà d'uso che ne può derivare sono notevoli. Speriamo che in futuro i nostri amici di Microsoft si inventino qualcosa di più semplice e chiaro.
Spero di aver comunque portato qualche contributo di chiarezza.

Sorgenti
L'articolo è corredato del codice sorgente delle soluzioni studiate, sia in VB.Net che in C#, scaricabile dall'area Download.

Feedback
Per ulteriori informazioni potete contattarmi al mio indirizzo o visitare il mio blog.