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:
- None: indica di fornire il file di configurazione comune applicato a tutti gli utenti, praticamente nomeapplicazione.exe.config.
- PerUserRoaming: indica di fornire il file di configurazione roaming applicato all'utente corrente.
- PerUserRoaming-AndLocal: indica di fornire il file di configurazione locale applicato all'utente corrente.
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.