Guarda! Senza mani! - Terza parte
a cura di Sabrina Cosolo e Diego Cattaruzza (requisiti: conoscenza intermedia di Sql Server e di .Net)La classe TAndTSettings
Usualmente nel framework le collection sono chiamate col nome dell'item che contengono e il suffisso Collection, ma poiché non amiamo i nomi troppo lunghi, ci limitiamo a mettere al plurale il nome dell'elemento per ottenere la sua collezione, così TandTSettings è una collezione di TAndTSetting, TAndTSettingCollection è troppo lungo per la nostra pigrizia. Per memorizzare una collezione di setting, costruiremo una collezione tipizzata derivata dalla collezione Generic List e, approfittando del fatto che la classe base fornisce già quasi tutto quel che serve, scriveremo solo il codice minimo per il nostro uso, la pigrizia regna sovrana.
Pure essendo pigri, è opportuno che per le nostre collezioni stabiliamo alcune regole di base, così come abbiamo fatto per le Entities. Pertanto, prima di stendere la nostra classe, predisponiamo un contratto che essa debba rispettare, quindi generiamo una Interfaccia. Quest'interfaccia si chiamerà IEntitiesCollection e tutte le collection che la implementeranno avranno i seguenti obblighi:
- Implementare il metodo CompareTo
- Implementare l'evento INotifyPropertyChanged
Ovviamente, questo serve per propagare quanto già predisposto per gli elementi della collezione, in modo che l'oggetto possa essere utilizzato come datasource per quanto ancora molto rozzo.
Per iniziare a generare la nostra classe, nel progetto TAndT.Base creiamo una nuova cartella che chiameremo Collections nella quale aggiungiamo un Item di tipo Interfaccia chiamandola IEntitiesCollection. Nel codice del file generato scriviamo quanto segue:
using System; using System.ComponentModel; namespace TAndT.Base.Collections { /// <summary> /// Interfaccia comune alle collezioni di entity /// </summary> public interface IEntitiesCollection : IComparable, INotifyPropertyChanged { } }Imports System Imports System.ComponentModel Namespace Collections Public Interface IEntitiesCollection Inherits IComparable, INotifyPropertyChanged End Interface End NamespaceCome possiamo vedere, il codice è minimo e il contratto indica che è necessario implementare IComparable e INotifyPropertyChanged (per quest'ultima interfaccia è opportuno impostare prima la direttiva using/Imports System.ComponentModel, come abbiamo fatto per l'interfaccia IEntity)
Torniamo sul Solution Explorer e creiamo TAndTSettings, la nostra nuova classe Collection sempre nella stessa cartella.
using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Xml.Serialization; using TAndT.Base.Entities; using TAndT.Base.Xml; namespace TAndT.Base.Collections { [Serializable, XmlRoot(ElementName= "TAndTSettings", Namespace = "http://www.visual-basic.it")] public class TAndTSettings : List<TAndTSetting>, IEntitiesCollection { } }Imports System Imports System.Collections.Generic Imports System.ComponentModel Imports System.Text Imports System.Xml.Serialization Imports TAndT.Base.Entities Namespace Collections <SerializableAttribute(), XmlRootAttribute(Namespace:="http://www.visual-basic.it", _ ElementName:="TAndTSettings")> _ Public Class TAndTSettings Inherits List(Of TAndTSetting) Implements IEntitiesCollection End Class End NamespaceAnche in questo caso, implementiamo l'attributo Serializable, per indicare che questa classe potrà essere salvata su disco tramite serializzazione.
La classe, come possiamo vedere, eredita da List<T> o List(of T), implementando la tipizzazione in base al nostro elemento Entity, il TAndTSetting.
Abbiamo aggiunto anche un altro attributo, XmlRoot, che ci permetterà di far sì che i file serializzati dalle nostre classi siano univoci e, seppure qualcun'altro nell'universo scrivesse una classe con lo stesso nome, i file XML che la serializzano sarebbero comunque diversi dalla nostra. Questo grazie al parametro Namespace. Aggiungiamo anche un altro parametro, ElementName, che ci permette di decidere il nome da dare all'elemento root del file XML prodotto dalla classe, questo perché, per default, essendo una collection il serializzatore standard del framework la chiamerebbe ArrayOfTandTSetting, mentre noi vogliamo che mantenga il suo nome, TAndTSettings.Nota pratica VB: prima di scrivere Implements IEntitiesCollection, scrivete Implements IComparable, Componentmodel.INotifyPropertyChanged, ottenendo automaticamente Function ed Event, e poi cambiate le implementazioni con le analoghe di IEntitiesCollection.
#region Variabili private private static readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; #endregion #region Events public event PropertyChangedEventHandler PropertyChanged; #endregion #region INotifyPropertyChanged Implementazione event handler protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { if (PropertyChanged != null) PropertyChanged(this, e); } #endregion#Region "Variabili private" Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name #End Region #Region "Eventi" <Category("ClassEvents"), Description("Notifica la modifica di una property")> _ Public Event PropertyChanged(ByVal sender As Object, _ ByVal e As System.ComponentModel.PropertyChangedEventArgs) _ Implements IEntitiesCollection.PropertyChanged #End Region #Region "Generatori di eventi" Private Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs) If Not e.PropertyName Is Nothing Then RaiseEvent PropertyChanged(Me, e) End If End Sub #End RegionQuesta prima parte è identica a quella della classe Entity (a parte l'interfaccia implementata), pertanto non occorrono ulteriori spiegazioni.
[XmlIgnore] public TAndTSetting this[string pName] { get { try { TAndTSetting retItem = null; for (int i = 0; i < this.Count; i++) { if (this[i].Name.ToLower() == pName.ToLower()) { retItem = this[i]; break; } } return (retItem); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } } }<XmlIgnore()> _ Default Public Overloads ReadOnly Property Item(ByVal nome As String) As TAndTSetting Get Try Dim retItem As TAndTSetting = Nothing For i As Integer = 0 To Me.Count - 1 If Me(i).Name.ToLower = nome.ToLower Then retItem = Me(i) Exit For End If Next Return retItem Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End Get End PropertyLa prima implementazione interessante è un metodo indexer, che ci permetterà di accedere alla collezione usando il Nome di un elemento, visto che è probabile sia il metodo per noi più comodo da usare.
L'indexer è una speciale property: in questo caso fornisce il metodo più semplice per cercare il dato che ci serve nella collection, non è efficiente, ma semplice da implementare, visto che questa collezione non conterrà molti elementi.
Facciamo notare: l'attributo [XmlIgnore], il quale indica alla funzione di serializzazione che questa proprietà non deve essere serializzata; che la proprietà è stata realizzata come ReadOnly; per ultimo, ma non meno importante, che la comparazione imposta è di tipo case insensitive, ovvero maiuscole e minuscole non sono considerate nella comparazione dei nomi. Non costituiscono un obbligo, ma, per l'uso che faremo di questa collezione, sono probabilmente le scelte migliori.
A parte, come discorso accademico, facciamo notare la differenza di verbosità nella firma, nei due linguaggi. Dalla firma VB si deduce che cosa in realtà sia un indexer: la proprietà Item di un insieme, considerata di default.public override string ToString() { StringBuilder sb = new StringBuilder(); foreach (TAndTSetting item in this) { sb.AppendLine(item.ToString()); } return (sb.ToString()); }Public Overrides Function ToString() As String Dim sb As New StringBuilder For Each item As TAndTSetting In Me sb.AppendLine(item.ToString) Next Return sb.ToString() End FunctionAnche il metodo ToString è implementato semplicemente, visto che facciamo solo una lista usando il ToString già predisposto nella classe TAndTSetting.
Omettiamo il codice per i metodi Equals, GetHashCode, e gli operatori di Uguaglianza(== [=]) e Disuguaglianza (!=[<>]) che sono praticamente identici a quelli già implementati nella classe TAndTSetting.
Invece il metodo CompareTo è diverso:public int CompareTo(object obj) { int ret = -1; try { if (obj is TAndTSettings) { TAndTSettings val = (TAndTSettings)obj; //Compara il numero di elementi delle collezioni int comparator = this.Count.CompareTo(val.Count); //Se il numero di elementi è uguale if (comparator == 0) { //Compara i singoli elementi for (int i = 0; i < this.Count; i++) { comparator = this[i].CompareTo(val[i]); //Se trovi un elemento diverso fermati che sono diverse. if (comparator != 0) break; } } ret = comparator; } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } return (ret); }Public Function CompareTo(ByVal obj As Object) As Integer _ Implements IEntitiesCollection.CompareTo Dim ret As Integer = -1 Try If TypeOf obj Is TAndTSettings Then Dim val As TAndTSettings = DirectCast(obj, TAndTSettings) ' compara il count Dim comparator As Integer = Me.Count.CompareTo(val.Count) If comparator = 0 Then ' i singoli elementi For i As Integer = 0 To Me.Count - 1 comparator = Me(i).CompareTo(val(i)) ' se diverso esci If comparator <> 0 Then Exit For Next End If ret = comparator End If Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try Return ret End FunctionQuesto metodo di comparazione è diverso, ma scopriremo che potrà essere copiato e incollato in tutte le collezioni dello stesso tipo che dovessimo implementare.
La comparazione tra le due collezioni inizia anzitutto verificando che gli oggetti siano del tipo corretto, se lo sono verifica che abbiano lo stesso numero di elementi, infine confronta ciascun elemento nello stesso ordine: se tutti gli elementi sono uguali nello stesso ordine, le collezioni sono uguali. Ovviamente questa convenzione è una scelta, per usi più sofisticati si potrebbero utilizzare funzioni di sort e comparare le due collezioni dopo l'ordinamento, ma per ora ci bastano le basi.
Non è detto che non torneremo su queste classi in seguito.Passiamo ad un'altra funzione interessante, la funzione che controlla se un elemento con un nome dato esiste nella collezione. Ci servirà per imporre un vincolo di unicità alla nostra collezione, perché l'Indexer funzioni correttamente e perché, per l'uso che ne faremo, è opportuno non avere nomi multipli.
public bool Exists(string pName) { bool retExists = false; try { retExists = this[pName] != null; } catch (Exception) { retExists = false; } return (retExists); }Public Overloads Function Exists(ByVal nome As String) As Boolean Dim retExists As Boolean = False Try retExists = (Me(nome) <> Nothing) Catch ex As Exception retExists = False End Try Return retExists End FunctionOra dobbiamo predisporre per TAndTSettings un override del metodo Add della List, che preveda un controllo sugli elementi duplicati, fenomeno da impedire, quindi ci serve un'eccezione tipizzata che possa essere intercettata e gestita, quando utilizzeremo la collezione nello sviluppo. In altre parole dobbiamo generare una nuova classe, DuplicateItemException, nella cartella Collections del progetto TAndT.Base:
using System; using System.Collections.Generic; using System.Text; namespace TAndT.Base.Collections { public class DuplicateItemException : System.ApplicationException { public DuplicateItemException(string pMessage) : base(pMessage) { } } }Namespace Collections Public Class DuplicateItemException Inherits System.ApplicationException Public Sub New(ByVal message As String) MyBase.New(message) End Sub End Class End NamespaceCome possiamo vedere, si tratta di una classe molto semplice che eredita da System.ApplicationException e non fa nulla se non implementare il suo costruttore, infatti non abbiamo bisogno che la nostra eccezione faccia nulla di diverso dalle normali eccezioni di sistema: la sua sola peculiarità è il fatto di avere un nome proprio, che ci permetterà poi di riconoscerla, intercettarla ed elaborarla nei nostri programmi.
Forziamo un override (anzi, un nascondimento) della funzione Add della collezione TAndTSettings in modo tale da imporre il fatto che gli elementi collezionati abbiano il vincolo di avere un nome univoco:
public new void Add(TAndTSetting pItem) { try { if (!Exists(pItem.Name)) { base.Add(pItem); } else { throw new DuplicateItemException(string.Format("L' elemento {0} esiste nella collezione", pItem.Name)); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shadows Sub Add(ByVal item As TAndTSetting) Try If Not Exists(item.Name) Then MyBase.Add(item) Else Throw New DuplicateItemException(String.Format( _ "L' elemento {0} esiste nella collezione", item.Name)) End If Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End SubAggiungiamo un'overload della funzione Add per poter aggiungere nuovi elementi passando semplicemente Nome e Valore dell'elemento:
public void Add(string pName, string pValue) { try { if (!Exists(pName)) { TAndTSetting item = new TAndTSetting(); item.Name = pName; item.Value = pValue; base.Add(item); } else { throw new DuplicateItemException(string.Format("L' elemento {0} esiste nella collezione", pName)); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shadows Sub Add(ByVal nome As String, ByVal valore As String) Try If Not Exists(nome) Then Dim item As New TAndTSetting item.Name = nome item.Value = valore MyBase.Add(item) Else Throw New DuplicateItemException(String.Format( _ "L' elemento {0} esiste nella collezione", nome)) End If Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End SubPer C#, il secondo metodo Add è un overload, ma in VB bisogna forzare l'override (Shadow) perché è forzato il primo metodo, il che è un comportamento incongruente, a meno che non ci sia qualcosa che non sappiamo.
Manca ancora una funzionalità nella nostra collezione, quella che ci permetterà di scrivere la collezione su un supporto fisico: abbiamo bisogno di poter salvare il contenuto della collezione su file e rileggere lo stesso contenuto da file.
Per fare questo, dobbiamo implementare due metodi, WriteXml e ReadXml, ma, prima, ci serve una classe helper, che ci permetta di serializzare qualunque oggetto.La classe SerializationHelper
Premettiamo che questa classe è una classe antica, fra quelle che Sabrina utilizza per il suo lavoro, risale agli sviluppi con il framework 1.1. Al momento non abbiamo tempo di sostituirla con una nuova classe implementata con i generics, che probabilmente sarebbe più efficiente. E' stata creata grazie ad un articolo presente sul sito, scritto un paio di anni or sono da Enrico Barillari.Per implementare questa classe, in verità ne creeremo quattro. Pertanto, per prepararci a tutto questo, sul Solution Explorer di Visual Studio andiamo ad aggiungere i seguenti nuovi oggetti nel progetto TAndT.Base.
EmptyPathException Una nuova classe, derivata da ApplicationException, che ci permette di tipizzare l'eccezione da restituire, se una stringa in cui ci aspettiamo di trovare il path di un file è invece vuota. NoFileException Una seconda classe, derivata da ApplicationException, che ci permette di tipizzare l'eccezione da restituire, quando cerchiamo un file che deve esistere e questo invece non si trova nel luogo specificato. IoHelper Una classe che conterrà le funzioni di controllo e verifica della correttezza dei file e della loro esistenza, che useranno le eccezioni di cui sopra. Xml Una nuova cartella sulla root del progetto, per identificare le classi che hanno specifica attinenza con la manipolazione dei dati Xml, in questa cartella andremo a porre la classe SerializationHelper. SerializationHelper Una nuova classe (in effetti il succo di questo articolo), con i metodi che permettono la serializzazione delle nostre classi su disco in formato XML.
Questa classe dovrà essere generata sotto la cartella Xml appena creata.Iniziamo ad osservare le tre piccole classi di supporto:
La classe EmptyPathException
using System; using System.Collections.Generic; using System.Text; namespace TAndT.Base { public class EmptyPathException : System.ApplicationException { public EmptyPathException(string pMessage) : base(pMessage) { } } }Public Class EmptyPathException Inherits System.ApplicationException Public Sub New(ByVal message As String) MyBase.New(message) End Sub End ClassQuesta classe, come possiamo notare, non fa proprio nulla, salvo che derivare dalla classe System.ApplicationException e implementarne il costruttore. Perché fare questo tipo di classe, invece di usare semplicemente l'ApplicationException con il messaggio giusto? E' specificato nelle linee guida, i cosiddetti Patterns di Microsoft, che questo è il metodo più corretto per implementare le eccezioni che noi decidiamo di lanciare all'interno di una applicazione.
Il motivo è semplice, ma proviamolo: aggiungiamo al menu HelperTests della FrmMain un menu 'ApplicationException' ed un menu 'EccezioneTipizzata'.
Nello scrivere il codice per il primo dei due, vedremo che, se usassimo una ApplicationException con un messaggio personalizzato, per poter intercettare questa specifica eccezione e gestirla nel codice, dovremmo fare la seguente cosa:private void applicationExceptionToolStripMenuItem_Click(object sender, EventArgs e) { try { //Ovviamente qui dovrebbe esserci del codice intelligente, ad esempio una richiesta //del nome del file all'utente con una OpenFileDialog. string nomefile = string.Empty; try { if (nomefile.Length == 0) { throw new ApplicationException("Il file specificato non è stato trovato"); } } catch (ApplicationException ex) { if (ex.Message == "Il file specificato non è stato trovato") { nomefile = "defaultdataoutput.xml"; } } } // ... qualcosa con nomefile valido catch (Exception ex) { Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex); } }Private Sub ApplicationExceptionToolStripMenuItem_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles ApplicationExceptionToolStripMenuItem.Click Try ' Ovviamente qui dovrebbe esserci del codice intelligente, ad esempio una richiesta ' del nome del file all'utente con una OpenFileDialog. Dim nomefile As String = String.Empty Try If (nomefile.Length = 0) Then Throw New ApplicationException("Il file specificato non è stato trovato") End If Catch ex As ApplicationException If ex.Message = "Il file specificato non è stato trovato" Then nomefile = "defaultdataoutput.xml" End If End Try ' ... qualcosa con nomefile valido Catch ex As Exception Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) End Try End SubQuindi saremmo costretti a gestire una comparazione su un messaggio, che a nostro giudizio è fra le comparazioni più inefficienti e pericolose, soprattutto se la stringa di eccezione si trovasse in un luogo diverso da quella di intercettazione e magari il programmatore appena arrivato nel team decidesse di migliorare l'italiano di coloro che fanno programmi.
Vediamo invece i benefici della nostra eccezione tipizzata:
private void eccezioneTipizzataToolStripMenuItem_Click(object sender, EventArgs e) { try { string nomefile = string.Empty; try { IoHelper.CheckEmptyPath(nomefile); } catch (EmptyPathException ex) { nomefile = "defaultdataoutput.xml"; } } catch (Exception ex) { Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex); } }Private Sub EccezioneTipizzataToolStripMenuItem_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles EccezioneTipizzataToolStripMenuItem.Click Try Dim nomefile As String = String.Empty Try IoHelper.CheckEmptyPath(nomefile) Catch ex As EmptyPathException nomefile = "defaultdataoutput.xml" End Try ' ... qualcosa con nomefile valido Catch ex As Exception Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) End Try End SubCome possiamo notare, non dobbiamo fare null'altro che intercettare l'eccezione giusta e risolvere il problema.
Per il momento, questo test è solo sulla carta, poiché la classe IoHelper non è ancora stata dotata di metodi.La classe NoFileException
Come la precedente è una classe di supporto per tipizzare le eccezioni e il suo scopo è lo stesso della precedente.using System; using System.Collections.Generic; using System.Text; namespace TAndT.Base { public class NoFileException : System.ApplicationException { public NoFileException(string pMessage) : base(pMessage) { } } }Public Class NoFileException Inherits System.ApplicationException Public Sub New(ByVal message As String) MyBase.New(message) End Sub End ClassNon crediamo siano necessarie ulteriori spiegazioni: ogni volta che ci servisse una nuova eccezione nelle nostre classi, implementeremo una di queste minuscole classi di eccezione, e le linee giuida dicono anche che l'eccezione tipizzata deve stare il più vicino possibile a ciò che la genera, pertanto in questo caso le eccezioni stanno nello stesso namespace della classe che le usa (la vedremo subito).
La classe IoHelper
Come le altre classi helper, anche questa è una classe statica, con una serie di metodi di controllo che utilizzeremo quando andremo a trattare apertura e uso di file su disco.using System; using System.Collections.Generic; using System.Text; namespace TAndT.Base { public static class IoHelper { private static readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; public static void CheckEmptyPath(string pPath) { if (pPath == null || pPath.Length == 0) { throw new EmptyPathException( "Non è stato specificato alcun valore per il parametro obbligatorio Path."); } } public static void CheckNoFile(string pPath) { if (!System.IO.File.Exists(pPath)) { throw new NoFileException(string.Format("Il file {0} non esiste.", pPath)); } } } }Public Class IoHelper Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name Public Shared Sub CheckEmptyPath(ByVal path As String) If path Is Nothing OrElse path.Length = 0 Then Throw New EmptyPathException( _ "Non è stato specificato alcun valore per il parametro obbligatorio Path.") End If End Sub Public Shared Sub CheckNoFile(ByVal path As String) If Not System.IO.File.Exists(path) Then Throw New NoFileException(String.Format("Il file {0} non esiste.", path)) End If End Sub End ClassQuesta classe non è molto complessa, e fa parte delle classi per programmatori pigri: ogni volta che ci servirà verificare se un file esiste (quando ad esempio lo chiediamo al nostro utente) oppure verificare che non ci abbia messo una stringa vuota come nome di un file, potremo avvisarlo in modo gentile, magari con una messagebox personalizzata e una icona adatta
(a proposito, adesso è possibile effettuare il test EccezioneTipizzata).
La classe SerializationHelper
Questa classe ci fornisce i servizi base di serializzazione in formato XML per qualsiasi oggetto noi intendiamo salvare su disco. Come abbiamo già detto, non è la più bella, né la più efficiente che si possa ottenere, ma fa bene il suo lavoro e per ora ci accontentiamo. Anche questa è una classe statica e contiene in tutto nove metodi, che ci danno modo di serializzare e deserializzare qualsiasi oggetto che abbia l'attributo Serializable o implementi le interfacce di serializzazione.using System; using System.Collections.Generic; using System.Text; using System.Xml; using System.Xml.Serialization; using System.IO; namespace TAndT.Base.Xml { public static class SerializationHelper { private static readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; } }Imports System Imports System.Collections.Generic Imports System.Text Imports System.Xml Imports System.Xml.Serialization Imports System.IO Namespace Xml Public Class SerializationHelper Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name End Class End NamespaceLa struttura base della classe è sempre la solita, vi invitiamo solo a osservare come abbiamo inserito fra le clausole using (Imports) System.Xml, System.Xml.Serialization e System.IO. I namespace di sistema che contengono le classi dedicate alla gestione di XML e dell'IO su file su disco.
Il primo metodo è BuildReader:public static XmlTextReader BuildReader(string pXmlString) { try { NameTable nt = new NameTable(); XmlNamespaceManager nsmgr = new XmlNamespaceManager(nt); nsmgr.AddNamespace("bk", "urn:sample"); XmlParserContext context = new XmlParserContext(null, nsmgr, null, XmlSpace.None); return (new XmlTextReader(pXmlString, XmlNodeType.Element, context)); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function BuildReader(ByVal xmlString As String) As XmlTextReader Try Dim nt As New NameTable Dim nsmgr As XmlNamespaceManager = New XmlNamespaceManager(nt) nsmgr.AddNamespace("bk", "urn:sample") Dim context As New XmlParserContext(Nothing, nsmgr, Nothing, XmlSpace.None) Return New XmlTextReader(xmlString, XmlNodeType.Element, context) Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIl metodo BuildReader ci serve per costruire un XmlTextReader a partire da una stringa XML, la sua ragion d'essere sta nel fatto che qualsiasi oggetto potrebbe essere serializzato su un file su disco, ma anche in memoria, per poter essere poi utilizzato come dato passato ad un WebService, oppure semplicemente memorizzato su un database invece che su un file su disco. Questa funzione, ricevuta una stringa contenente un oggetto serializzato in XML, la trasforma in un XmlTextReader, poi utilizzato normalmente dalla funzione di deserializzazione standard della classe.
Naturalmente, oltre che leggere, si deve poter scrivere:public static void SerializeToFile(string pPath, object pObjToSerialize, Type pTypeToSerialize) { try { using (XmlTextWriter writer = new XmlTextWriter(pPath, Encoding.UTF8)) { writer.Formatting = Formatting.Indented; writer.Indentation = 4; XmlSerializer serializer = new XmlSerializer(pTypeToSerialize); serializer.Serialize(writer, pObjToSerialize); writer.Close(); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Sub SerializeToFile(ByVal path As String, ByVal objToSerialize As Object, _ ByVal typeToserialize As Type) Try Using writer As New XmlTextWriter(path, Encoding.UTF8) writer.Formatting = Formatting.Indented writer.Indentation = 4 Dim serializer As New XmlSerializer(typeToserialize) serializer.Serialize(writer, objToSerialize) writer.Close() End Using Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End SubIl metodo SerializeToFile, molto semplice, serializza un oggetto su file, si aspetta che sia un oggetto serializzabile e nulla più, questa funzione di serializzazione è perfetta per serializzare un oggetto semplice, ad esempio una classe Entity. Non è invece adatto per la serializzazione diretta di Collezioni, perché non ha modo di essere informato di quali sono gli oggetti contenuti in una collezione. Ma niente paura, alle collezioni e agli oggetti complessi, pensa questo suo overload:
public static void SerializeToFile(string pPath, object pObjToSerialize, Type pTypeToSerialize, Type[] pExtraTypes) { try { using (XmlTextWriter writer = new XmlTextWriter(pPath, Encoding.UTF8)) { writer.Formatting = Formatting.Indented; writer.Indentation = 4; XmlSerializer serializer = new XmlSerializer(pTypeToSerialize, pExtraTypes); serializer.Serialize(writer, pObjToSerialize); writer.Close(); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Sub SerializeToFile(ByVal path As String, ByVal objToSerialize As Object, _ ByVal typeToSerialize As Type, ByVal extraTypes As Type()) Try Using writer As New XmlTextWriter(path, Encoding.UTF8) writer.Formatting = Formatting.Indented writer.Indentation = 4 Dim serializer As New XmlSerializer(typeToSerialize, extraTypes) serializer.Serialize(writer, objToSerialize) writer.Close() End Using Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End SubQuesto secondo metodo SerializeToFile permette di indicare, oltre all'oggetto di base, la lista di tutti i tipi di dati che l'oggetto contiene, pertanto, quando si tratta di una collezione tipizzata o di un oggetto che a sua volta contiene altri oggetti, nella lista pExtraTypes indicheremo tutti i tipi di dati che il metodo si deve aspettare di trovare, dandogli quindi informazioni su dove trovare le informazioni necessarie a serializzare anche questi oggetti complessi.
Il metodo DeserializeFromFile ci permette di rileggere quanto scaricato su file con il primo metodo SerializeToFile, cioè un oggetto serializzato su disco.
public static object DeserializeFromFile(Type pTypeToDeserialize, string pPath) { try { using (XmlTextReader reader = new XmlTextReader(pPath)) { XmlSerializer serializer = new XmlSerializer(pTypeToDeserialize); object retObj = serializer.Deserialize(reader); reader.Close(); return (retObj); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function DeserializeFromFile(ByVal typeToDeserialize As Type, _ ByVal path As String) As Object Try Using reader As New XmlTextReader(path) Dim serializer As New XmlSerializer(typeToDeserialize) Dim retObj As Object = serializer.Deserialize(reader) reader.Close() Return retObj End Using Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionE questo è il suo overload con le informazioni sui tipi:
public static object DeserializeFromFile(Type pTypeToDeserialize, Type[] pExtraTypes, string pPath) { try { using (XmlTextReader reader = new XmlTextReader(pPath)) { XmlSerializer serializer = new XmlSerializer(pTypeToDeserialize, pExtraTypes); object retObj = serializer.Deserialize(reader); reader.Close(); return (retObj); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function DeserializeFromFile(ByVal typeToDeserialize As Type, _ ByVal extraTypes As Type(), _ ByVal path As String) As Object Try Using reader As New XmlTextReader(path) Dim serializer As New XmlSerializer(typeToDeserialize, extraTypes) Dim retObj As Object = serializer.Deserialize(reader) reader.Close() Return retObj End Using Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIl secondo metodo DeserializeFromFile, ci permette di rileggere un oggetto complesso scaricato su file, ed è quindi complementare al secondo metodo SerializeToFile.
public static string SerializeToString(object pObjToSerialize) { try { using (MemoryStream stream = new MemoryStream()) { XmlSerializer serializer = new XmlSerializer(pObjToSerialize.GetType()); serializer.Serialize(stream, pObjToSerialize); string retStr = Encoding.UTF8.GetString(stream.ToArray()); stream.Close(); return (retStr); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function SerializeToString(ByVal objToSerialize As Object) As String Try Using stream As New MemoryStream Dim serializer As New XmlSerializer(objToSerialize.GetType) serializer.Serialize(stream, objToSerialize) Dim retStr As String = Encoding.UTF8.GetString(stream.ToArray) stream.Close() Return retStr End Using Catch ex As ApplicationException Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIl metodo SerializeToString, invece di generare un file per memorizzare il nostro oggetto serializzato, genera una stringa; questo ci permetterà, volendo, di archiviare la nostra stringa su un campo di un database, semplicemente come testo, e rileggerla poi con comodo. Per quanto SQL Server permetta anche di generare campi di tipo XML al suo interno, questo metodo non ha alcuna attinenza con la memorizzazione su database di campi XML. Come già detto, esso trasforma esclusivamente un oggetto in una stringa xml. Magari da passare ad un Webservice.
Questo è il suo overload per oggetti complessi (con le informazioni sui tipi):public static string SerializeToString(object pObjToSerialize, Type[] pExtraTypes) { try { using (MemoryStream stream = new MemoryStream()) { XmlSerializer serializer = new XmlSerializer(pObjToSerialize.GetType(), pExtraTypes); serializer.Serialize(stream, pObjToSerialize); string retStr = Encoding.UTF8.GetString(stream.ToArray()); stream.Close(); return (retStr); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function SerializeToString(ByVal objToSerialize As Object, _ ByVal extraTypes As Type()) As String Try Using stream As New MemoryStream Dim serializer As New XmlSerializer(objToSerialize.GetType, extraTypes) serializer.Serialize(stream, objToSerialize) Dim retStr As String = Encoding.UTF8.GetString(stream.ToArray) stream.Close() Return retStr End Using Catch ex As ApplicationException Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionEd ecco i metodi di deserializzazione. Prima il metodo DeserializeFromString complementare al primo metodo SerializeToString:
public static object DeserializeFromString(Type pTypeToDeserialize, string pXmlString) { try { using (XmlReader xr = BuildReader(pXmlString)) { XmlSerializer serializer = new XmlSerializer(pTypeToDeserialize); object retObj = serializer.Deserialize(xr); xr.Close(); return (retObj); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function DeserializeFromString(ByVal typeToSerialize As Type, _ ByVal xmlString As String) As Object Try Using xr As XmlReader = BuildReader(xmlString) Dim serializer As New XmlSerializer(typeToSerialize) Dim retObj As Object = serializer.Deserialize(xr) xr.Close() Return retObj End Using Catch ex As ApplicationException Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionQuindi il suo overload, complementare del secondo metodo SerializeFromString, per oggetti complessi:
public static object DeserializeFromString(Type pTypeToDeserialize, Type[] pExtraTypes, string pXmlString) { try { using (XmlReader xr = BuildReader(pXmlString)) { XmlSerializer serializer = new XmlSerializer(pTypeToDeserialize, pExtraTypes); object retObj = serializer.Deserialize(xr); xr.Close(); return (retObj); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Shared Function DeserializeFromString(ByVal typeToSerialize As Type, _ ByVal extraTypes As Type(), _ ByVal xmlString As String) As Object Try Using xr As XmlReader = BuildReader(xmlString) Dim serializer As New XmlSerializer(typeToSerialize, extraTypes) Dim retObj As Object = serializer.Deserialize(xr) xr.Close() Return retObj End Using Catch ex As ApplicationException Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionI metodi di serializzazione di TAndTSettings
Abbiamo terminato di sviluppare il necessario a discutere gli ultimi metodi relativi alla nostra classe TAndTSettings.
Pertanto, torniamo a svilupparla, aggiungendo la direttivausing TAndT.Base.XmlImports TAndT.Base.Xmle implementando il metodo WriteXml:
public void WriteXml(string pXmlPath) { try { IoHelper.CheckEmptyPath(pXmlPath); SerializationHelper.SerializeToFile(pXmlPath, this, typeof(TAndTSettings), new Type[] { typeof(TAndTSetting) }); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Sub WriteXml(ByVal xmlPath As String) Try IoHelper.CheckEmptyPath(xmlPath) SerializationHelper.SerializeToFile(xmlPath, Me, Me.GetType, New Type() {GetType(TAndTSetting)}) Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End SubLa prima cosa che osserviamo è che abbiamo chiamato il metodo helper per il controllo del path, però non intercettiamo affatto l'eccezione tipizzata, ma, anzi, la rilanciamo alla classe chiamante all'interno di una semplice ApplicationException. Perché?
In questo caso ciascuno può fare quel che preferisce, noi abbiamo deciso di fare in questo modo perché la funzione usata per la visualizzazione dell'eccezione nella gestione della User Interface scava da sola nelle eccezioni che le sono inviate e, quindi, darà direttamente una serie di informazioni all'utente su dove si è verificata l'eccezione, permettendo di informare il programmatore a riguardo, oltre alle informazioni date dalla eccezione tipizzata eventualmente lanciata dal metodo helper..Oltre a ciò, possiamo osservare che siccome la nostra collezione è un oggetto complesso, utilizziamo la funzione di serializzazione oggetti complessi e indichiamo alla stessa che, oltre al tipo principale TAndTSettings, all'interno della nostra classe troverà anche il tipo TAndTSetting. Potrebbe non essere necessario, se tutti gli oggetti sono serializzabili, ma informazioni in più non fanno di certo male al compilatore. Può darsi che, quando stenderemo la funzione di test, proveremo a usare anche le altre funzioni.
public string WriteXml() { try { return (SerializationHelper.SerializeToString(this, new Type[] { typeof(TAndTSetting) })); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Function WriteXml() As String Try Return SerializationHelper.SerializeToString(Me, New Type() {GetType(TAndTSetting)}) Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIl secondo metodo di serializzazione, overload del precedente, ci permette di serializzare la classe su stringa. Anche in questo caso abbiamo usato la serializzazione per oggetti complessi e la stringa prodotta potrà essere usata come dato per memorizzare l'oggetto su database o per passarlo ad un webservice o qualsivoglia altro tipo d'uso.
Questo è il metodo di deserializzazione:
public static TAndTSettings ReadXml(string pXml, bool pIsXmlData) { TAndTSettings ret = null; try { if (!pIsXmlData) { IoHelper.CheckEmptyPath(pXml); IoHelper.CheckNoFile(pXml); ret = (TAndTSettings)SerializationHelper.DeserializeFromFile(typeof(TAndTSettings), new Type[] { typeof(TAndTSetting) }, pXml); } else { ret = (TAndTSettings)SerializationHelper.DeserializeFromString(typeof(TAndTSettings), new Type[] { typeof(TAndTSetting) }, pXml); } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } return (ret); }Public Shared Function ReadXml(ByVal xml As String, ByVal isXmlData As Boolean) As TAndTSettings Dim ret As TAndTSettings = Nothing Try If Not isXmlData Then IoHelper.CheckEmptyPath(xml) IoHelper.CheckNoFile(xml) ret = DirectCast(SerializationHelper.DeserializeFromFile(GetType(TAndTSettings), _ New Type() {GetType(TAndTSetting)}, xml), TAndTSettings) Else ret = DirectCast(SerializationHelper.DeserializeFromString(GetType(TAndTSettings), _ New Type() {GetType(TAndTSetting)}, xml), TAndTSettings) End If Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try Return ret End FunctionPer la deserializzazione, possiamo notare come ci basti un solo metodo, e soprattutto che questo metodo, a differenza di quelli di serializzazione, è un metodo static (Shared), che possa cioè essere chiamato senza avere un oggetto instanziato. Abbiamo fatto questa scelta perché il metodo stesso restituisce un oggetto TAndTSettings, quindi è opportuno che il metodo sia statico, in modo da poterlo utilizzare per ottenere direttamente un oggetto TAndTSettings da un file o da una stringa.
In questo caso, inoltre, non avremmo potuto fare due diversi metodi in overload, perché avrebbero avuto la medesima firma. Infatti, per deserializzare un oggetto le informazioni che possiamo fornire al metodo sono una stringa, che in un caso contiene un nome di file e in un altro contiene l'oggetto serializzato in XML, quindi abbiamo utilizzato un parametro booleano per informare il metodo su quanto contenuto nel parametro stringa passato. Comunque, in entrambi i casi otteniamo come valore di ritorno un oggetto TAndTSettings.
Anche in questo caso utilizziamo le funzioni dell' IOHelper, per controllare il nome e l'esistenza del file di serializzazione e deleghiamo al chiamante l'intercettazione dell'errore e il suo trattamento.Il test
Con questo oseremmo dire che abbiamo costruito la nostra classe per la gestione base dei setting. Quindi, prima di passare ad una classe che ci permetta di usarla per memorizzare sia i setting di applicazione che quelli di utente, andiamo a fare qualche test per vedere se ciò che abbiamo scritto funziona.
Prima aggiungiamo alla FrmMain la direttiva:using TAndT.Base.Collections;Imports TAndT.Base.CollectionsPoi aggiungiamo il menu 'Test TAndTSettings' al menu 'Setting Manager Tests' della FrmMain:
private void testTAndTSettingsToolStripMenuItem_Click(object sender, EventArgs e) { try { StringBuilder sb = new StringBuilder(); sb.AppendLine("Test TAndTSettings"); TAndTSettings settings = new TAndTSettings(); sb.AppendFormat("La classe vuota contiene {0} elementi e la funzione ToString() ritorna {1}", settings.Count, settings.ToString()); sb.AppendLine(); TAndTSetting setting = new TAndTSetting(); setting.Name = "ServerName"; setting.Value = "MyComputer"; settings.Add(setting); settings.Add("Database", "MyDatabase"); sb.AppendLine( "Inseriti due elementi provando entrambe le funzioni add, la classe contiene:"); sb.AppendLine(settings.ToString()); TAndTSettings newSettings = new TAndTSettings(); newSettings.Add("ServerName", "MyComputer"); sb.AppendLine( "Generata una nuova collection con un solo elemento, la funzione di comparazione vale:"); sb.AppendFormat("settings.CompareTo(newSettings) = {0}", settings.CompareTo(newSettings)); sb.AppendLine(); newSettings.Add("Database", "MyDatabase"); sb.AppendLine( "Aggiunto il secondo elemento alla classe, le due collezioni dovrebbero essere uguali:"); sb.AppendFormat("settings.CompareTo(newSettings) = {0}", settings.CompareTo(newSettings)); sb.AppendLine(); sb.AppendLine("Testiamo l'esistenza del setting ServerName:"); sb.AppendFormat("settings.Exists(\"ServerName\") = {0}", settings.Exists("ServerName")); sb.AppendLine(); sb.AppendLine("Serializziamo la classe sul file Mysettings.xml"); settings.WriteXml("MySettings.xml"); newSettings = TAndTSettings.ReadXml("MySettings.xml", false); sb.AppendLine("Deserializziamo la classe sul file Mysettings.xml"); sb.AppendLine("La classe generata contiene:"); sb.AppendLine(newSettings.ToString()); Warnings.Info(sb.ToString()); } catch (Exception ex) { Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex); } }Private Sub TestTAndTSettingsToolStripMenuItem_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles TestTAndTSettingsToolStripMenuItem.Click Try Dim sb As New StringBuilder sb.AppendLine("Test TAndTSettings") Dim settings As New TAndTSettings sb.AppendFormat("La classe vuota contiene {0} elementi e la funzione ToString() ritorna {1}", _ settings.Count, settings.ToString()) sb.AppendLine() Dim setting As New TAndTSetting setting.Name = "ServerName" setting.Value = "MyComputer" settings.Add(setting) settings.Add("Database", "MyDatabase") sb.AppendLine("Inseriti due elementi provando entrambe le funzioni add, la classe contiene:") sb.AppendLine(settings.ToString()) Dim newSettings As New TAndTSettings newSettings.Add("ServerName", "MyComputer") sb.AppendLine( _ "Generata una nuova collection con un solo elemento, la funzione di comparazione vale:") sb.AppendFormat("settings.CompareTo(newSettings) = {0}", settings.CompareTo(newSettings)) sb.AppendLine() newSettings.Add("Database", "MyDatabase") sb.AppendLine( _ "Aggiunto il secondo elemento alla classe, le due collezioni dovrebbero essere uguali:") sb.AppendFormat("settings.CompareTo(newSettings) = {0}", settings.CompareTo(newSettings)) sb.AppendLine() sb.AppendLine("Testiamo l'esistenza del setting ServerName:") sb.AppendFormat("settings.Exists(""ServerName"") = {0}", settings.Exists("ServerName")) sb.AppendLine() sb.AppendLine("Serializziamo la classe sul file Mysettings.xml") settings.WriteXml("MySettings.xml") newSettings = TAndTSettings.ReadXml("MySettings.xml", False) sb.AppendLine("Deserializziamo la classe sul file Mysettings.xml") sb.AppendLine("La classe generata contiene:") sb.AppendLine(newSettings.ToString()) Warnings.Info(sb.ToString()) Catch ex As Exception Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) End Try End SubQuesto codice fa un test base sulle funzionalità della nostra collezione, visualizzandone i risultati in una messagebox.
Non abbiamo testato la gestione delle eccezioni, ma lasciamo a chi proverà il progetto il compito di testare se percorsi vuoti o inesistenti o dati errati provocano le dovute eccezioni.Appuntamento alla prossima puntata
Adesso che abbiamo costruito il necessario per salvare dei setting, abbiamo bisogno di qualcosa in più. Infatti, per quel che riguarda la memorizzazione dei dati di connessione a SQL Server, abbiamo la necessità di memorizzare sia dati a livello applicazione sia dati a livello utente. Pertanto, per fare questo, sempre a imitazione dei setting Microsoft, predisporremo due diversi file XML, uno con i dati di applicazione, uguali per tutti gli utenti, che porremo nella cartella del programma, uno invece contenente i dati specifici dell'utente, che metteremo a livello di dati di configurazione utente.
Per fare questo, nella prossima puntata creeremo quanto necessario a memorizzare tutti i tipi di Setting, salvarli su disco nel luogo più opportuno e rileggerli..
Naturalmente, il codice fin qui prodotto è disponibile in area Download.
Potete scrivere Feedback (commenti, critiche, suggerimenti, correzioni) sul blog di Sabrina.