Guarda! Senza mani! - Quarta parte
a cura di Sabrina Cosolo e Diego Cattaruzza (requisiti: conoscenza intermedia di Sql Server e di .Net)
Premessa
Adesso che abbiamo costruito le due classi dati (TAndTSetting e TAndTSettings), che ci permettono di memorizzare i nostri setting, possiamo utilizzarle nella nostra applicazione. Però, esaminando che cosa è necessario fare per utilizzare i dati in una applicazione, abbiamo verificato che ci servono una serie di metodi aggiuntivi.
Dapprima abbiamo pensato di inserirli in una classe apposita, TAndTSettingsManager, poi ci siamo resi conto che non ce n'è bisogno: basta aggiungerli alla collezione TAndTSettings e poi utilizzarli nella classe che li metterà a disposizione dell'applicazione che li utilizzerà.Alcuni metodi in più per TAndTSettings
I metodi che abbiamo trovato necessario implementare per le funzionalità da far svolgere alla classe gestore della configurazione sono i seguenti:
GetSetting Restituisce un setting dato il suo nome, se esiste, oppure una stringa vuota, in caso contrario. SetSetting Aggiorna un setting, se esiste, o ne crea uno nuovo, se non esiste GetDatatable Genera una datatable dalla collection per nutrire una Datagrid SetFromDatatable Legge una datatable e aggiorna la collection LoadSettings Genera una collection o la carica a partire da un file XML SaveSettings Salva su file la collection verificando prima di sovrascrivere l'output Per rendere affidabili i metodi GetDatatable e SetFromDatatable, si rende necessaria una gestione dei valori DbNull, con cui tutte le classi che parlano con i database devono avere a che fare. Quindi cominciamo col creare un'altra classe helper.
La classe FieldValue
Al progetto TAndT.Base aggiungiamo (tasto destro, Add>New Folder>) una cartella di nome Data. Da questa cartella (tasto destro, Add>Class>) aggiungiamo una nuova classe di nome FieldValue.
Come sopra detto, questa classe helper serve per la gestione dei fastidiosi quanto necessari DbNull. Una strategia, usata da molti programmatori che conosco per liberarsi dal problema dei DbNull, consiste nel fare in modo che tutti i campi di una tabella debbano per forza essere valorizzati, ma, senza DbNull, non possiamo gestire relazioni, senza relazioni, addio all'integrità referenziale e, allora, che diamine lavorano a fare i programmatori che creano gli RDBMS, se poi noi usiamo il 5% del loro lavoro?
Noi siamo fra coloro che non hanno paura dei DbNull, ma questo non toglie che non sia necessario fare qualche acrobazia nel codice visto che DbNull non è null e neppure Nothing. Questa piccola, ma molto importante, classe, è opera di Rudy Azzan, che lavora con Sabrina ed ha realizzato questo oggetto per evitare i controlli da farsi su ogni metodo, funzione, classe che abbia a che vedere con i dati quando si tratta di verificare o assegnare il valore di un campo database ad una variabile. Per realizzare tutto questo, si sono usati i Generics e, ancora una volta, le classi e i metodi statici.Questa è la struttura esterna della classe di Rudy:
using System; using System.Collections.Generic; using System.Text; namespace TAndT.Base.Data { public static class FieldValue<T> where T : IComparable { private static readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; ... } }Imports System Imports System.Collections.Generic Imports System.Text Namespace Data Public Class FieldValue(Of T As IComparable) Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name ... End Class End NamespaceOltre alla dichiarazione d'uso di tre namespace di base del framework e alla collocazione della classe nel namespace adatto, possiamo notare una prima stranezza nella stessa dichiarazione del tipo FieldValue. Infatti, c'è una clausola where accanto al nome della classe, e poi la dicitura T : IComparable (Of T As IComparable). Cosa significa? Significa che la nostra classe è un tipo Generic, cui però non è possibile assegnare qualsiasi oggetto: i soli oggetti ammessi sono quelli che implementano l'interfaccia IComparable, cioè quelli che possono essere direttamente comparati.
Confusi? Vediamo allora il codice che contiene, così la confusione aumenterà (ma poi tutto sembrerà semplice quando la classe verrà usata).public static T Value(object pData, T pDefaultValue) { try { T outValue = pDefaultValue; DBNull nullo = pData as DBNull; if (nullo == null) { outValue = (T)pData; } return outValue; } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function Value(ByVal dato As Object, ByVal defaultValue As T) As T Try Dim outValue As T = defaultValue Dim nullo As DBNull = TryCast(dato, DBNull) If nullo Is Nothing Then outValue = DirectCast(dato, T) End If Return outValue Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionLa prima versione del metodo Value. Il parametro pData (dato) è l'oggetto proveniente dal database (cioè un generico object) e pDefaultValue è il valore che vogliamo sia restituito se il valore di pData è DbNull. Questo metodo è generico, ci permette di processare un dato in modo tipizzato - di qualsiasi tipo, purché aderente ad IComparable - e verificare se questo dato è DbNull, nel qual caso è possibile assegnargli il valore di default indicato. In altre parole, se a questo metodo viene passato un oggetto del tipo specificato, viene restituito il suo valore, se invece il dato fosse DbNull, verrebbe restituito il valore indicato come default.
Ad esempio, con:string name = FieldValue<string>.Value(row[FLD_NAME], string.Empty);
informiamo la classe che ci aspettiamo che il campo FLD_NAME sia una stringa, e che vogliamo ci venga restituito il valore string.Empty, se l'oggetto che gli passiamo, su cui deve fare il controllo, è DbNull.
public static T Value(object pData) { try { return FieldValue<T>.Value(pData, default(T)); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function Value(ByVal dato As Object) As T Try Return FieldValue(Of T).Value(dato, Nothing) Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End TryLa seconda versione, tanto per rendere le cose più vivaci, è implementata in modo tale da non necessitare del valore di default. Infatti, utilizza una delle funzionalità aggiunte alla specifica 2.0 di C#, cioè la default(T), una funzioncina simpatica che inizializza qualsiasi tipo di con il valore di default imposto dal suo costruttore di base senza parametri. Pertanto per la string sarà null (Nothing), per l'intero e tutti i numerici sarà 0, e così via. (Per VB non serve sprecarsi a scrivere 'default': è... di default)
Anche se non li abbiamo ancora utilizzati, e forse neppure ci serviranno, dato che siamo programmatori seri
, implementiamo anche i metodi complementari, che restituiscono un object con valore DbNull se il valore del dato passato è null (Nothing) o uguale al valore che viene ritenuto pari a null (Nothing) (per regole di business, ad esempio).
Quindi implementiamo un primo metodo che prende un valore da un controllo e lo converte in un valore adatto al database oppure in DbNull...public static object TryParseToDBNull(T pData, T pDbNullValue) { try { object objRet = null; if ((pDbNullValue == null && pData == null) || pData.CompareTo(pDbNullValue) == 0) { objRet = DBNull.Value; } else { objRet = pData; } return objRet; } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function TryParseToDBNull(ByVal dato As T, ByVal dbNullValue As T) As Object Try Dim objRet As Object = Nothing If dbNullValue Is Nothing AndAlso dato Is Nothing OrElse dato.CompareTo(dbNullValue) = 0 Then objRet = DBNull.Value Else objRet = dato End If Return objRet Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIn questo codice, se entrambi i parametri passati hanno valore null , oppure il valore da noi considerato equivalente a null, viene restituito un object DbNull, altrimenti viene restituito un object comunque valido per il database.
public static object TryParseToDBNull(T pData) { try { return FieldValue<T>.TryParseToDBNull(pData, default(T)); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function TryParseToDBNull(ByVal dato As T) As Object Try Return FieldValue(Of T).TryParseToDBNull(dato, Nothing) Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionLa seconda versione del metodo utilizza la prima passando il valore di default del tipo di dato da esaminare.
Abbiamo così concluso la definizine della classe helper.
Riprendiamo quindi il codice della classe TAndTSettings, per implementare i metodi elencati all'inizio.using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Xml.Serialization; using TAndT.Base.Entities; using TAndT.Base.Xml; using System.Data; using TAndT.Base.Data; using System.IO;Imports System Imports System.Collections.Generic Imports System.ComponentModel Imports System.Text Imports System.Xml.Serialization Imports TAndT.Base.Entities Imports TAndT.Base.Xml Imports System.Data Imports System.IO Imports TAndT.Base.DataPer prima cosa, è necessario aggiornare le direttive using (Imports) per i nuovi metodi: TAndT.Base.Xml, System.Data (qualcuno dirà 'finalmente!'), System.IO e TAndT.Base.Data, appena definito. I due namespace di sistema ci servono per l'uso dell'oggetto DataTable e dell'oggetto File.
public string GetSetting(string pName) { string retSetting = string.Empty; if (Exist(pName)) { retSetting = this[pName].Value; } return (retSetting); }Function GetSetting(ByVal name As String) As String Dim retSetting = String.Empty If ExistSetting(name) Then retSetting = Me(name).Value End If Return retSetting End FunctionCome possiamo vedere, questo metodo restituisce una stringa vuota oppure il setting richiesto se esiste. Si potrebbe anche utilizzare il semplice indexer, ma aggiungere il controllo dell'esistenza serve per essere certi di non avere una eccezione.
public void SetSetting(string pName, string pValue) { if (this.Exist(pName)) { this[pName].Value = pValue; } else { TAndTSetting stt = new TAndTSetting(); stt.Name = pName; stt.Value = pValue; this.Add(stt); } }Public Sub SetSetting(ByVal name As String, ByVal value As String) If ExistSetting(name) Then Me(name).Value = value Else Dim stt As New TAndTSetting stt.Name = name stt.Value = value Me.Add(stt) End If End SubQuesto metodo invece controlla se esiste un setting e lo aggiorna oppure lo aggiunge.
Nota: abbiamo ritenuto opportuno modificare il nome del metodo Exists in ExistSetting, dato che non è un overload, anche se ha lo stesso nome di un metodo della classe base, perché non vi fa riferimento.public const string FLD_NAME = "Name"; public const string FLD_VALUE = "Value";Public Const FLD_NAME As String = "Name" Public Const FLD_VALUE As String = "Value"Per i prossimi due metodi, per prima cosa generiamo due costanti con i nomi dei campi della collezione, che diverranno le colonne che inseriremo in una DataTable, le costanti sono utili ovunque dobbiamo riferirci a questi oggetti per evitare errori di digitazione.
public DataTable GetDataTable(string pTableName) { try { System.Data.DataTable dt = new System.Data.DataTable(); dt.TableName = pTableName; dt.Columns.Add(FLD_NAME, typeof(string)); dt.Columns.Add(FLD_VALUE, typeof(string)); dt.PrimaryKey = new System.Data.DataColumn[] { dt.Columns[FLD_NAME] }; foreach (TAndTSetting stt in this) { System.Data.DataRow dr = dt.NewRow(); dr[FLD_NAME] = stt.Name; dr[FLD_VALUE] = stt.Value; dt.Rows.Add(dr); } return (dt); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Function GetDataTable(ByVal tableName As String) As DataTable Try Dim dt As New DataTable dt.TableName = tableName dt.Columns.Add(FLD_NAME, Type.GetType("System.String")) dt.Columns.Add(FLD_VALUE, Type.GetType("System.String")) dt.PrimaryKey = New DataColumn() {dt.Columns(FLD_NAME)} For Each stt As TAndTSetting In Me Dim dr As DataRow = dt.NewRow dr(FLD_NAME) = stt.Name dr(FLD_VALUE) = stt.Value dt.Rows.Add(dr) Next Return dt Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIl primo metodo genera una DataTable che contiene quanto inserito nella collection.
public void SetFromDataTable(System.Data.DataTable pDt, bool pClearCollection) { try { if (pClearCollection) { Clear(); } for (int i = 0; i < pDt.Rows.Count; i++) { System.Data.DataRow drV = pDt.Rows[i]; string name = FieldValue<string>.Value(drV[FLD_NAME]); string value = FieldValue<string>.Value(drV[FLD_VALUE], string.Empty); if (name != null && name.Trim().Length > 0) { SetSetting(name, value); } } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Sub SetFromDataTable(ByVal dt As DataTable, ByVal clearCollection As Boolean) Try If clearCollection Then Clear() End If For i As Integer = 0 To dt.Rows.Count - 1 Dim drV As DataRow = dt.Rows(i) Dim name As String = FieldValue(Of String).Value(drV(FLD_NAME)) Dim value As String = FieldValue(Of String).Value(drV(FLD_VALUE), String.Empty) If Not name Is Nothing AndAlso name.Trim.Length > 0 Then SetSetting(name, value) End If Next Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End SubIl secondo metodo aggiorna la collection da una DataTable, con l'opzione per resettare la collection prima di aggiornarla.
L'opzione per l'azzeramento della collection è stata inserita perché, usualmente, i setting non prevedono l'aggiunta o l'eliminazione di valori da parte dell'utente, anche se la datagrid è il mezzo più semplice per modificare i setting. Pertanto non accade quasi mai che ci siano cancellazioni ed aggiunte di righe. Infatti, come vedremo nell'implementazione della classe settings della nostra applicazione, la generazione dei setting viene lasciata usualmente ad una funzione "hardcoded".public static bool LoadSettings(string pFileName, ref TAndTSettings pSettings) { try { bool retFound = false; if (pSettings == null) { pSettings = new TAndTSettings(); } if (File.Exists(pFileName)) { TAndTSettings sttList = TAndTSettings.ReadXml(pFileName, false); foreach (TAndTSetting stt in sttList) { pSettings.SetSetting(stt.Name, stt.Value); } retFound = true; } return (retFound); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function LoadSettings(ByVal fileName As String, ByRef settings As TAndTSettings) _ As Boolean Try Dim retFound As Boolean = False If settings Is Nothing Then settings = New TAndTSettings End If If File.Exists(fileName) Then Dim sttList As TAndTSettings = TAndTSettings.ReadXml(fileName, False) For Each stt As TAndTSetting In sttList settings.SetSetting(stt.Name, stt.Value) Next retFound = True End If Return retFound Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionQuesto metodo aggiorna i dati della collezione, creandola nel caso non fosse ancora stata instanziata, traendoli dal file XML di cui viene passato il percorso.
Perché salvare ciò che è nella collezione? Perché, per la sicurezza del funzionamento dell'applicazione, è opportuno che sia l'applicazione stessa a generare tutti i setting assegnando loro un valore di default. Inoltre, la lettura da file XML aggiorna solamente i dati esistenti, in modo che, se ad una revisione di applicazione vengono aggiunti una serie di setting nuovi, la collezione risulterà contenere anche questi e tutti quelli vecchi aggiornati in base alla situazione impostata sulla macchina dell'utente. Se invece dovessimo eliminare un setting (per quanto cosa non usuale) esso non verrà eliminato dal file XML e se non usato non darà alcun fastidio alla nostra applicazione. Se riterremo opportuno eliminare i setting non usati, potremo farlo in modo specifico nella nostra applicazione caricando ciò che è sul file xml, verificando le chiavi che non ci interessano, cancellandole dalla collezione e salvando con sovrascrittura del vecchio file.
Facciamo notare la specifica ref per il parametro della collection che indica il passaggio per riferimento, in modo tale che la classe chiamante, trovi nella sua collection il risultato della funzione di caricamento.public static bool SaveSettings(string pFileName, bool pOverwrite, TAndTSettings pSettings) { bool retSaved = false; try { bool okToWork = true; if (File.Exists(pFileName) && !pOverwrite) { okToWork = false; } if (okToWork) { pSettings.WriteXml(pFileName); retSaved = true; } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } return (retSaved); }Public Shared Function SaveSettings(ByVal fileName As String, ByVal overWrite As Boolean, _ ByVal settings As TAndTSettings) As Boolean Dim retSaved As Boolean = False Try Dim okToWork As Boolean = True If File.Exists(fileName) AndAlso Not overWrite Then okToWork = False End If If okToWork Then settings.WriteXml(fileName) retSaved = True End If Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try Return retSaved End FunctionQuesto metodo effettua il salvataggio dei setting: verifica ed eventualmente sovrascrive i setting esistenti, il flag di sovrascrittura è stato inserito perché questa funzione può essere usata, ad esempio, per esportare i setting, quindi può essere utile in qualche caso non sovrascrivere ciò che esiste, ma magari indicare un file diverso.
Il test
Facciamo un test della nostra classe usando la nostra FrmMain. Aggiungiamo un'opzione di menu, dal titolo Test TAndTSettings Addendum al menu Setting Manager Tests, facciamo doppio click in modo da implementare il codice del metodo testTAndTSettingsAddendumToolStripMenuItem. Ma prima aggiungiamo la direttiva:using System.IO;Imports System.IOprivate void testTAndTSettingsAddendumToolStripMenuItem_Click(object sender, EventArgs e) { try { string addname = "c:\\temp\\addtest.xml"; if (File.Exists(addname)) File.Delete(addname); TAndTSettings sttMgr = null; TAndTSettings.LoadSettings(addname, ref sttMgr); StringBuilder sb = new StringBuilder(); sb.AppendLine("Settings Addendum test 1 creazione"); sb.AppendFormat("AppSettings Number: {0}", sttMgr.Count); sb.AppendLine(); Warnings.Info(sb.ToString()); sb = new StringBuilder(); sttMgr.Add("PrimoValore", "Dato Applicativo 1"); sttMgr.Add("SecondoValore", "Dato Applicativo 2"); sttMgr.Add("TerzoValore", "Dato Applicativo 3"); sb.AppendLine("Settings Addendum test 2 Aggiunta dati"); sb.AppendLine(sttMgr.ToString()); Warnings.Info(sb.ToString()); TAndTSettings.SaveSettings(addname, true, sttMgr); Warnings.Info("Settings Addendum test3 salvataggio"); sb = new StringBuilder(); sb.AppendLine("Settings Addendum test 4 Clear"); sttMgr.Clear(); sb.AppendFormat("Settings Number: {0}", sttMgr.Count); sb.AppendLine(); Warnings.Info(sb.ToString()); TAndTSettings.LoadSettings(addname, ref sttMgr); sb = new StringBuilder(); sb.AppendLine("Settings Addendum test 5 Rilettura dati"); sb.AppendLine(sttMgr.ToString()); Warnings.Info(sb.ToString()); } catch (Exception ex) { Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex); } }Private Sub TestTAndTSettingsAddendumToolStripMenuItem_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles TestTAndTSettingsAddendumToolStripMenuItem.Click Try Dim addName As String = "c:\temp\addtest.xml" If File.Exists(addName) Then File.Delete(addName) End If Dim sttMgr As TAndTSettings = Nothing TAndTSettings.LoadSettings(addName, sttMgr) Dim sb As New StringBuilder sb.AppendLine("Settings Addendum test 1 creazione") sb.AppendFormat("AppSettings Number: {0}", sttMgr.Count) sb.AppendLine() Warnings.Info(sb.ToString()) sb = New StringBuilder sttMgr.Add("PrimoValore", "Dato Applicativo 1") sttMgr.Add("SecondoValore", "Dato Applicativo 2") sttMgr.Add("TerzoValore", "Dato Applicativo 3") sb.AppendLine("Settings Addendum test 2 Aggiunta dati") sb.AppendLine(sttMgr.ToString()) Warnings.Info(sb.ToString()) TAndTSettings.SaveSettings(addName, True, sttMgr) Warnings.Info("Settings Addendum test3 salvataggio") sb = New StringBuilder() sb.AppendLine("Settings Addendum test 4 Clear") sttMgr.Clear() sb.AppendFormat("Settings Number: {0}", sttMgr.Count) sb.AppendLine() Warnings.Info(sb.ToString()) TAndTSettings.LoadSettings(addName, sttMgr) sb = New StringBuilder() sb.AppendLine("Settings Addendum test 5 Rilettura dati") sb.AppendLine(sttMgr.ToString()) Warnings.Info(sb.ToString()) Catch ex As Exception Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) End Try End SubPer fare un test di base, abbiamo generato la collezione, vuota, aggiunto tre valori, salvato la collezione, azzerato la collezione, ricaricato i dati salvati su file. I risultati del test sono riportati nei messagebox che abbiamo inserito allo scopo.
Nota: si abbia cura, nel proprio codice, di fornire un percorso esistente alla variabile addName.
Arrivederci alla prossima puntata
Probabilmente abbiamo concluso lo sviluppo di questa classe. Nella prossima puntata creeremo il contenitore per i setting della nostra applicazione e quanto necessario a renderlo visibile ad ognuno dei suoi componenti.
Il codice fin qui prodotto è disponibile in Area Download.
Potete scrivere Feedback (commenti, critiche, suggerimenti, correzioni) sul blog di Sabrina.