Qualsiasi applicazione che usa i database avrà bisogno di permettere di configurare la stringa di connessione al database e memorizzarla per ogni installazione client. Inoltre, potreste avere la necessità di memorizzare degli altri parametri utente come ad esempio qualche preferenza relativa a come l’interfaccia utente si deve comportare. Oltre a ciò potreste voler memorizzare automaticamente e salvare su disco lo Stato di una applicazione in modo che quando questa riparte l’utente la ritrovi come l’ha lasciata. Per fare tutto questo, nelle librerie di uso comune che ho aggiornato nel post precedente a questo ci sono già le classi adatte a memorizzare questi dati, che sono spiegate a partire da questo articolo:
Due classi per memorizzare velocemente parametri applicativi.
E negli articoli successivi ad esso, che sono opportunamente raggruppati in una stessa categoria come subcategoria delle Serie, all’interno del blog che le ospita, viene spiegata la User Interface per la creazione e gestione automatica dei setting in una applicazione.
Premessa
Quando ho iniziato questo articolo, un mese fa, volevo semplicemente usare le classi già spiegate nella serie di articoli indicata qui sopra e aggiungerle alla nostra applicazione UserDb, però, grazie al fatto che a causa del lavoro non ho avuto tempo da dedicarvi è passato un po’ di tempo e siccome nel frattempo ho ricevuto un paio di richieste su come si fa, in modo semplice, senza dover leggere dieci articoli, ho deciso che spiegherò il modello base, con le ruote di legno in questo articolo, poi lo evolveremo utilizzando le classi più Professional che ho sviluppato due o tre anni or sono per una lunga serie di articoli che si chiama MiniSqlAgent presente sul blog e useremo questi componenti per creare una UI generica in grado di gestire qualsiasi tipo di setting.
Parliamo di motivazioni
Innanzi tutto, tutti voi vi chiederete perché non utilizzare i setting applicativi forniti gratuitamente da Microsoft ed utilizzare quindi setting.setting o app.config.
Principalmente per 2 motivi:
- Avere il completo controllo della vostra applicazione e di quello che fa.
- Sfortunatamente la manutenzione dei setting applicativi ed utente quando l’applicazione viene aggiornata è quantomeno complicata.
Questo perché, se la vostra applicazione cambia versione, i vecchi setting utente vanno persi, ed è necessario implementare dei meccanismi che vadano a prendere tali setting e li riportino nella nuova installazione, se aggiornate l’applicazione con un setup, l’app.config viene sovrascritto se presente nel setup. Non da ultimo, perché non c’è la possibilità di creare una user interface per modificarli, sarà necessario che vengano modificati manualmente sui file xml ed è anche logico, visto che app.config si trova sulla cartella di installazione programmi che nessuno salvo un amministratore di sistema ha il permesso di modificare.
Sono certa che ci sono modi intelligenti per evitare quanto ho illustrato qui sopra, che sono documentati su MSDN e chi è abituato ad utilizzarli, mi può mandare un paio di link ed un esempio d’uso che sarò molto lieta di pubblicare.
Inoltre, chi è convinto che far modificare i parametri di configurazione di una applicazione da un file XML sia il modo corretto di lavorare, non ha mai lavorato su applicazioni dedicate ad utenti che sono abbastanza spigliati con un computer da poter installare una applicazione, eccezionalmente capaci nel loro lavoro, ma che di certo non hanno alcuna intenzione di imparare a capire come è fatto e come si modifica un file XML (o, come di moda adesso, un file JSON) a mano, pertanto pretendono che gli si fornisca una adeguata interfaccia utente per parametrizzare la propria applicazione.
Il mio commento è sempre, perché devo complicarmi la vita utilizzando qualcosa che è stato disegnato principalmente per strumenti molto più complessi da gestire delle mie semplici applicazioni?
Da qui l’uso di qualcosa di molto più terra terra assieme a 2 cartelle di sistema che chi ha progettato windows ha messo a nostra disposizione appositamente.
Dove mettere i setting di una applicazione
All’inizio di questo millennio, a qualunque programmatore sarebbe stato detto: Mettili nel Registry. Era fatto apposta. Sfortunatamente si è appurato nel tempo che il registry era una bella cosa, ma era anche pericoloso. Pertanto usare il Registry è praticabile ma non considerato “In”. Io non ho mai pensato di usarlo sempre a causa della mia mania di essere responsabile, e quindi di controllare tutto quello che la mia applicazione fa all’interno del computer, in modo tale da sapere con la massima sicurezza cosa è successo se per caso non funziona.
Windows possiede due cartelle, anche esse introdotte credo con la versione 2000 di windows, o giù di li:
- Program Data
- AppData
Program Data, che sul disco si chiama così anche se poi il file browser di Windows (dopo che avrete attivato l’opzione per vedere i file nascosti, perché se volete fare i programmatori è opportuno che sappiate farlo) traduce automaticamente il suo nome (sinceramente non so come si chiama in italiano perché uso sempre windows in inglese quindi chiedo aiuto a chi legge per dirmi il suo nome).
Per mostrarvelo, ho deciso di usare la finestra console, uno strumento che come programmatori dovreste saper usare ed ho utilizzato due comandi di derivazione DOS:
- cd \ (Change directory root per spostarmi sulla root directory del disco C:)
- dir program*. /A H (lista la directory mostrandomi tutte le cartelle il cui nome inizia per program comprese quelle Hidden)
Se aprite il file manager di windows, per poter vedere ProgramData, dovete andare sulle opzioni di cartella ed attivare la visualizzazione delle cartelle nascoste, oltre a questo vi consiglio, sempre se volete programmare, per divertimento o professione, di attivare, nella stessa finestra, la visualizzazione delle estensioni dei files in modo da poter distinguere quali files state guardando su una cartella da qualcosa di diverso dall’icona di default del file stesso.
Per poter scrivere su ProgramData, una applicazione deve avere permessi amministrativi, ma tutte le applicazioni e tutti gli utenti possono leggere il suo contenuto. Ecco perché è il luogo adatto a memorizzare qualsiasi file di configurazione che deve essere modificato solo da un Amministratore di Sistema.
AppData, è una cartella specifica dell’utente, pertanto ogni utente ha la sua, quindi la troviamo sotto la cartella root dell’utente, quindi in C:\Users\NomeUtente.
Nel mio caso, oltre ad AppData c’è una cartella che si chiama Application Data, che vi prego di ignorare, è stata prodotta dall’installazione di una applicazione i cui programmatori non sanno come chiedere il nome di AppData al sistema e che l’hanno generata per inserirvi i loro dati applicativi.
Anche AppData è una cartella nascosta, a cui le applicazioni installate hanno accesso e dove possono scrivere. In questo caso, ogni utente ed ogni applicazione installata da un utente ha il permesso di scrivere su questa cartella e le sue sottocartelle, per questo la cartella AppData è il posto giusto ove andare a scrivere i dati di configurazione delle preferenze utente, automatiche o modificabili manualmente che siano.
In realtà, .Net ha un helper di sistema che ci aiuterà ad utilizzare correttamente queste cartelle, senza dover sapere il loro nome esatto onde evitare di fare cose come la cartella presente sul mio disco.
Come è fatto un setting
Per memorizzare un setting, di qualsiasi tipo di setting si tratti, avete bisogno di 2 campi:
- ID una stringa che conterrà un valore che identifica in modo univoco il valore.
- Value una stringa che conterrà il valore vero e proprio.
Per una dimostrazione in modello basico, creeremo quindi le classi base per la gestione dei setting direttamente nella nostra User Interface dell’applicazione UserDb,
Essendo delle classi ad uso esclusivo della UI, non andrò ad inserirle nel progetto Entities, ma le creerò direttamente nel progetto eseguibile UsersDb, per enfatizzare che non sono Entities legate ai dati, ho creato una apposita cartella per contenerle, che ho chiamato guardacaso UIEntities.
Creiamo una classe all’interno della cartella chiamata DnwSetting, in modo che poi potremo sostituirla con quella full optional delle librerie Dotnetwork. Al suo interno, oltre ad implementare INotifyPropertyChanged perché se volessimo usare questa classe in WPF saremmo sicuri che tutto funzionerebbe a livello UI, creiamo le 2 property necessarie a salvare un setting.
[XmlAttribute] public string ID { get { return mID; } set { mID = value; OnPropertyChanged(FLD_ID); } } [XmlElement] public string Value { get { return mValue; } set { mValue = value; OnPropertyChanged(FLD_Value); } }
Eccole qui, ID e Value, tutto quel che serve per memorizzare un setting su un file di testo, oltre a ciò, se non avete mai letto uno dei miei tediosissimi articoli sulla serializzazione XML, quindi vi chiedete a cosa servono i 2 attributi che precedono la definizione della property, sono li per formattare correttamente il file XML in modo che siamo in grado di capire come è fatto anche quando lo aprissimo e leggessimo su disco.
Considerato che ci serviranno più valori, anche solo UserDb ha bisogno di 3 stringhe di connessione ed un opzione per indicare quale dei database è stato attivato, andiamo ad implementare una classe Collection per contenere la nostra lista dei setting.
Creerò quindi un ulteriore cartella nel progetto UserDb che chiamerò UICollections dove andrò a creare la mia collezione di settings.
public class DnwSettingsCollection: List<DnwSetting> { [XmlIgnore] public DnwSetting this[string ID] { get { return (this.FirstOrDefault(item => item.ID == ID)); } } public void WriteXml(string outputPath) { XmlWriterSettings xstt = new XmlWriterSettings(); xstt.ConformanceLevel = ConformanceLevel.Document; xstt.Encoding = Encoding.UTF8; xstt.Indent = true; xstt.IndentChars = new string(' ', 4); xstt.NewLineChars = "\r\n"; xstt.NewLineHandling = NewLineHandling.None; xstt.NewLineOnAttributes = false; xstt.OmitXmlDeclaration = false; using (XmlWriter writer = XmlWriter.Create(outputPath, xstt)) { //Predispongo i namespaces validi XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); ns.Add(string.Empty, "http://www.dotnetwork.it"); XmlSerializer serializer = new XmlSerializer(typeof(DnwSettingsCollection), new Type[] { typeof(DnwSetting)} ); // e questo é tutto ciò che serve per persistere i dati serializer.Serialize(writer, this, ns); writer.Close(); } } public static DnwSettingsCollection ReadXml(string inputPath) { DnwSettingsCollection ret = default(DnwSettingsCollection); using (XmlReader xr = XmlReader.Create(inputPath)) { XmlSerializer serializer = new XmlSerializer(typeof(DnwSettingsCollection), new Type[] { typeof(DnwSetting)}); ret = (DnwSettingsCollection)serializer.Deserialize(xr); } return (ret); } }
Ho copiato l’intera classe perché è molto semplice come vedete, contiene al suo interno un Indexer e i due metodi per Scrivere il contenuto su disco in XML e leggere il contenuto dal disco in XML. Considerato che l’articolo è dedicato ai beginner, rispiego (perché sono sicura che c’è almeno in un altro paio di articoli), che cos’è e come è fatto ognuno dei tre elementi della classe.
Il collection indexer
[XmlIgnore] public DnwSetting this[string ID] { get { return (this.FirstOrDefault(item => item.ID == ID)); } }
Un Indexer è una property inserita in una collection che permette di ricercare un elemento in tale collection utilizzando un criterio diverso da quello di default per la collection (usualmente l’indice numerico). Se avete guardato il codice della collection, potete osservare che DnwSettingsCollection è una classe derivata da List<T> ovvero da una Generic list di .Net, probabilmente la collection più usata assieme al Dictionary. Questa collection fornisce solo un indexer, quello numerico, pertanto
DnwSettingsCollection sttColl = new DnwSettingsCollection(); DnwSetting stt = sttColl[0];
Il codice qui sopra sarebbe del tutto possibile da utilizzare ma non molto pratico non sapendo l’indice del parametro che vogliamo leggere. L’aggiunta dell’indexer, ci permette di poter fare questo:
DnwSettingsCollection sttColl = new DnwSettingsCollection(); DnwSetting stt = sttColl["CnStringSqlServer"];
La property che abbiamo definito, aggiunge alla nostra collection la possibilità di ricercare un elemento conoscendone il suo ID univoco.
Il metodo WriteXml
public void WriteXml(string outputPath) { XmlWriterSettings xstt = new XmlWriterSettings(); xstt.ConformanceLevel = ConformanceLevel.Document; xstt.Encoding = Encoding.UTF8; xstt.Indent = true; xstt.IndentChars = new string(' ', 4); xstt.NewLineChars = "\r\n"; xstt.NewLineHandling = NewLineHandling.None; xstt.NewLineOnAttributes = false; xstt.OmitXmlDeclaration = false; using (XmlWriter writer = XmlWriter.Create(outputPath, xstt)) { //Predispongo i namespaces validi XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); ns.Add(string.Empty, "http://www.dotnetwork.it"); XmlSerializer serializer = new XmlSerializer(typeof(DnwSettingsCollection), new Type[] { typeof(DnwSetting)} ); // e questo é tutto ciò che serve per persistere i dati serializer.Serialize(writer, this, ns); writer.Close(); } }
WriteXml come dice il suo nome, è in grado di scrivere su un file di testo del nostro disco, il contenuto della collection in XML. Ho copiato in modo semplificato il metodo che trovate anche nelle librerie, la prima parte inizializza un XmlWriterSettings, ovvero un oggetto che indica alla classe XmlWriter, che si occupa della scrittura in XML su disco come vogliamo che sia formattata la nostra classe, quindi salviamo utilizzando UTF8, creiamo un file con le indentazioni e multilinea, usando CR+LF come separatore di riga, ed una stringa di 4 spazi come indentazione, però non inseriamo una nuova linea per ogni attributo. Vi ricordo che scrivere su disco in XML il contenuto di una classe che poi possa essere riletto ricreando la classe nello stato in cui è stata salvata si chiama Serializzare.
Il metodo ReadXml
public static DnwSettingsCollection ReadXml(string inputPath) { DnwSettingsCollection ret = default(DnwSettingsCollection); using (XmlReader xr = XmlReader.Create(inputPath)) { XmlSerializer serializer = new XmlSerializer(typeof(DnwSettingsCollection), new Type[] { typeof(DnwSetting)}); ret = (DnwSettingsCollection)serializer.Deserialize(xr); } return (ret); }
Il metodo per la lettura del file dal disco è molto più semplice, infatti non dobbiamo dirgli come il file è formattato perché .Net è abbastanza intelligente da arrangiarsi con qualsiasi tipo di formattazione trovi, al contrario di alcuni sistemi con cui mi sono scontrata negli anni, dove per leggere una stringa XML se il formato non era esattamente quello desiderato, non veniva riconosciuta. Pertanto al reader diamo solo 2 informazioni, ovvero i 2 tipi di dato che si troverà nel file xml e sono sicura che anche se non indicassimo DnwSetting, si arrangerebbe ugualmente.
[XmlRoot( ElementName = "DnwSettingsCollection")] public class DnwSettingsCollection: List<DnwSetting> {
Aggiungo ancora un piccolo elemento alla collection, ovvero indico come voglio che sia chiamato l’elemento Root del file XML generato. E scrivo qualche riga di codice per testare la classe sulla nostra applicazione così che possiate vedere il prodotto che otterremo, e nel prossimo articolo andremo a creare la classe per gestire i setting dal vero e una mini user interface per modificarli e vedremo come memorizzarli sulle cartelle opportune.
<?xml version="1.0" encoding="utf-8"?> <DnwSettingsCollection> <DnwSetting ID="CnSqlServer"> <Value>Server=localhost;Database=UsersDb;Trusted_Connection=True;Persist Security Info=True</Value> </DnwSetting> <DnwSetting ID="CnAccess"> <Value>Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\TestAccessDb\UsersDb.accdb;Persist Security Info=True;</Value> </DnwSetting> <DnwSetting ID="CnSqLite"> <Value>Data Source=C:\TestSqliteDb\UsersDb.sqlite;Version=3;</Value> </DnwSetting> <DnwSetting ID="SelectedDataSource"> <Value>SqlServer</Value> </DnwSetting> </DnwSettingsCollection>
Intanto vediamo il prodotto della serializzazione che ha le seguenti caratteristiche:
- Inizia con la dichiarazione del tipo XML in modo che qualsiasi applicazione sappia come trattarlo.
- Il Root element del file ha il nome da noi inserito nell’attributo XmlRoot.
- L’elemento ID è inserito come Attributo dell’elemento DnwSetting, grazie all’attributo XmlAttribute che abbiamo inserito sulla property.
- L’Elemento Value invece è inserito come Elemento dell’elemento DnwSetting che viene assegnato per default.
Riepilogo
In questo articolo abbiamo discusso i seguenti argomenti
- quali sono i luoghi più opportuni ove porre i parametri di configurazione (setting) di una applicazione.
- Come creare la classe che memorizza un parametro
- Come creare una collezione di parametri
- Come scrivere su disco e leggere da disco il contenuto della classe tramite la serializzazione XML.
Potete scaricare il progetto esempio a corredo al link seguente.
Per qualsiasi domanda, approfondimento, discussione potete usare il link alla form di contatto in cima alla pagina.