Press "Enter" to skip to content

Due classi per memorizzare velocemente parametri applicativi

Due classi che ci permetteranno di memorizzare elementi Key -> Value con descrizione e categoria per salvare informazioni di configurazione personalizzate per le applicazioni.

Tutti sanno come sono fatti e come funzionano i setting applicativi Microsoft, che possono essere inseriti ed utilizzati dalle nostre applicazioni. Chi segue questo blog sa altresì che a me non piace usarli. Perché sono troppo legati al funzionamento di .Net, mentre a me piace avere la libertà di fare le cose come mi piace. Pertanto nelle librerie di base Dotnetwork inserirò un entità ed una collection che possano memorizzare questo tipo di informazioni.

libraries_settings_01[7]

Le tre classi che abbiamo aggiunto alle librerie di base Dotnetwork.

La classe DnwSetting

un parametro di configurazione usualmente è un oggetto di tipo Key -> Value, quindi un codice ed un valore, però siccome siamo dei bravi programmatori, possiamo prevedere che daremo modo ai nostri utenti di modificare questi parametri utilizzando un interfaccia utente user friendly, non li obbligheremo a modificare un file XML in modo diretto. Pertanto, nella nostra classe prevediamo anche alcune informazioni accessorie a quelle strettamente indispensabili, quindi una descrizione per rendere comprensibile all’utente di cosa si tratta, una categoria, così se avessimo bisogno di molti valori possiamo raggrupparli logicamente, infine un tipo di editor, che utilizzeremo per poter in seguito generare una user Interface efficiente. Vediamo quindi le property della nostra classe Entity:

[XmlAttribute]
public string ID
{
	get
	{
		return mID;
	}
	set
	{
		mID = value;
		OnPropertyChanged(FLD_ID);
	}
}

[XmlAttribute]
public string Category
{
	get
	{
		return mCategory;
	}
	set
	{
		mCategory = value;
		OnPropertyChanged(FLD_Category);
	}
}

public string Description
{
	get
	{
		return mDescription;
	}
	set
	{
		mDescription = value;
		OnPropertyChanged(FLD_Description);
	}
}

public string Value
{
	get
	{
		return mValue;
	}
	set
	{
		mValue = value;
		OnPropertyChanged(FLD_Value);
	}
}

[XmlAttribute(AttributeName="EditorType")]
public string XmlEditorType
{
	get
	{
		return mEditorType.ToString();
	}
	set
	{
		EditorType tp = EditorType.TextBox;
		Enum.TryParse<EditorType>(value, out tp);
		mEditorType = tp;
		OnPropertyChanged(FLD_EditorType);
	}
}

[XmlIgnore]
public EditorType EditorType
{
	get
	{
		return mEditorType;
	}
	set
	{
		mEditorType = value;
		OnPropertyChanged(FLD_EditorType);
	}
}

Abbiamo predisposto una serie di property molto simili a quelle che abbiamo già definito nelle classi entity dei post precedenti di questo blog, non ho incluso tutto il contenuto della classe per non tediarvi, lo trovate ovviamente nel codice a corredo, ma volevo mostrare questa porzione di codice per discutere alcune cose.

[XmlAttribute] : Questo attributo fa in modo che la property non venga serializzata come un elemento XML (<NomeProperty>valore</NomeProperty> ma venga inserita come attributo del Tag che definisce la classe: <NomeClasse NomeProperty=ValoreProperty> questo rende il codice XML più compatto.

[XmlAttribute(AttributeName="EditorType")]
public string XmlEditorType

[XmlIgnore]
public EditorType EditorType

La porzione della dichiarazione delle due property qui sopra ci mostra un modo per ovviare ad un problema che può essere introdotto quando la variabile serializzata non è una stringa. L’ho fatto in questo caso con una enumerazione, ma potrebbe essere effettuato con un numero o con una data allo stesso modo. Quello che ho fatto è stato indicare all’ Xml Serializer di ignorare la property di tipo enumerato ed invece serializzare una property di tipo stringa, in modo tale che quando la stringa viene poi riletta dall’Xml Deserializer, viene effettuato un controllo del suo contenuto e se non fosse realmente un elemento dell’enumerazione, invece di dare un errore, verrebbe utilizzato il valore di Default.

Perché non fare un controllo direttamente sulla property reale? Perché se il contenuto del file XML non fosse un elemento dell’enumerazione l’errore verrebbe sollevato prima di assegnare la property.

La classe DnwSettingsCollection

La collezione dei nostri parametri di configurazione sarà l’oggetto che scriveremo su disco e rileggeremo per predisporre i parametri di configurazione delle nostre applicazioni, la useremo anche per poter generare o modificare i parametri di configurazione, pertanto sarà una collezione di tipo Observable.

[XmlRoot(Namespace = "http://www.dotnetwork.it", ElementName = "DnwSettingsCollection")]
public class DnwSettingsCollection : ObservableCollection<DnwSetting>
{
	....
}

La classe, per cui abbiamo definito il namespace di destinazione e il nome da assegnare all’elemento root del file XML contenente la collection.

public DnwSetting this[string itemID]
{
	get
	{
		return this.FirstOrDefault(item => item.ID == itemID);
	}
}

L’indexer per ID, ci permetterà di accedere agli elementi della collezione usando il loro ID invece di utilizzare l’ indice numerico che in questo caso non ha alcun significato.

public void WriteXml(string xmlPath, bool indentOutput = true, string newlineCharsOutput = "\r\n")
{
	try
	{
		XmlHelper.SerializeToFile(xmlPath, this, 
			typeof(DnwSettingsCollection), new Type[] { typeof(DnwSetting) },  noNamespaces: false, indent: indentOutput, newlineChars: newlineCharsOutput);
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

public string WriteXml(bool indentOutput = true, string newlineCharsOutput = "\r\n")
{
	try
	{
		return (XmlHelper.SerializeToString(this, new Type[] { typeof(DnwSetting) },
			noNamespaces: false, indent: indentOutput, newlineChars: newlineCharsOutput));
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Le funzioni di Serializzazione, così come abbiamo fatto per JSON, abbiamo costruito un helper per la serializzazione XML (Già discusso nelle librerie di uso comune della versione 2.0 del framework) che riporteremo di seguito spiegando le sue funzionalità. Possiamo notare come nei due metodi utilizzati noi forniamo al serializzatore il contenuto ed il tipo degli oggetti da serializzare e una serie di parametri di formattazione per decidere come dovrà essere generato l’XML sul file di destinazione.

public static DnwSettingsCollection ReadXml(string xmlData, bool isXmlData)
{
	DnwSettingsCollection ret = null;
	try
	{
		if (!isXmlData)
		{
			ret = (DnwSettingsCollection)XmlHelper.DeserializeFromFile(
			typeof(DnwSettingsCollection), xmlData, new Type[] { typeof(DnwSetting) });
		}
		else
		{
			ret = (DnwSettingsCollection)XmlHelper.DeserializeFromString(
			typeof(DnwSettingsCollection), xmlData, new Type[] { typeof(DnwSetting) });
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
	return (ret);
}

La funzione di deserializzazione che permette di rigenerare una collection dal contenuto di una stringa o da quello di un file.

La classe XmlHelper

Corrispondente alla stessa classe in versione JSON ci permette di serializzare o deserializzare una classe in XML. Vediamo nel dettaglio i suoi metodi:

public static string SerializeToString(object objToSerialize, Type[] extraTypes = nullbool noNamespaces = false, string elementsPrefix = nullList<KeyValuePair<string, string>> namespaces = null, bool indent = trueint indentation = 4, string newlineChars = "\r\n", bool useBOM = true)
{
	string ret = "";
	try
	{
		using (MemoryStream stream = new MemoryStream())
		{
			using (XmlWriter writer = XmlWriter.Create(stream, GetWriterSettings(indent, indentation, newlineChars, useBOM)))
			{
				XmlSerializer serializer = new XmlSerializer(objToSerialize.GetType(), extraTypes);

				//Predispongo i namespaces validi
				XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
				string prefix = elementsPrefix != null ? elementsPrefix : string.Empty;
				if (noNamespaces)
				{
					ns.Add(prefix, TXT_StandardNamespace);
				}
				else
				{
					if (namespaces != null)
					{
						foreach (KeyValuePair<string, string> nams in namespaces)
						{
							string prf = nams.Value == null ? string.Empty : nams.Value;
							ns.Add(prf, nams.Key);
						}

					}
				}

				serializer.Serialize(writer, objToSerialize, ns);
				ret = Encoding.uTF8.GetString(stream.ToArray());
				writer.Close();
			}
			stream.Close();
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
	return (ret);
}

Abbiamo predisposto per il metodo di serializzazione su stringa tutta una serie di parametri opzionali per la definizione del formato dei dati generati, ed utilizzando un oggetto XmlWriterSettings abbiamo la possibilità di decidere in ogni sua parte l’aspetto del file XML prodotto. Vediamo quindi di cosa si tratta:

  • noNamespaces = Parametro boolean che ci permette di creare un file XML o una stringa XML che non contenga alcuna definizione di tipo xmlns: al suo interno, è utile quando l’XML prodotto può essere usato per scambiare dati con terze parti (ad esempio con un webservice) e per evitare complicazioni visto che i sistemi che producono i dati sono eterogenei, si preferisce tenere solo l’ ossatura XML senza namespaces o altri elementi specifici.
  • elementsPrefix = Parametro che permette di indicare una stringa prefisso per gli oggetti, siamo usualmente abituati a trovare questo tipo di prefisso ad esempio su XAML oppure su ASP.Net, inserire una stringa in questo parametro fa in modo che gli oggetti serializzati siano dichiarati con tale prefisso: (es. <prefix:Myclass><MyProperty1>value</MyProperty1><MyProperty2>value2</MyProperty2></prefix/MyClass>).
  • namespaces = Al contrario del parametro precedente serve per forzare l’inserimento di uno o più attributi xmlns: In questo caso come nel primo caso l’utilità si ha nello scambio dati con terze parti ove il destinatario o mittente dei dati XML abbia definito dei namespace specifici nelle proprie classi XML.
  • indent = Indica se l’XMLWriter deve indentare le righe della classe in base ai tag, se è falso, tutte le righe inizieranno a colonna 1 se è vero saranno inseriti degli spazi per creare le indentazioni in base alla profondità dell’elemento dell’oggetto.
  • indentation = Indica di quanti spazi indentare ogni livello.
  • newlineChars = un altro parametro di formattazione, in questo caso è stato inserito perché sempre nello scambio dati con terze parti, alcuni sistemi (solitamente quelli più obsoleti) non sono in grado di leggere file che contengono i classici carriage return e newline oppure vogliono dei separatori di riga diversi.
  • usaBOM = Indica se inserire o meno in testa al file XML i due caratteri di controllo che segnalano il Byte Order Mark, che stabilisce l’ endianness della codifica e il tipo di codifica utilizzato (uTF) se stiamo lavorando con dei Webservices e spediamo i dati usando POST o GET, il BOM crea dei problemi non indifferenti, pertanto deve essere rimosso.
private static XmlWriterSettings GetWriterSettings(bool indent, int indentation, string newlineChars, bool useBOM)
{
	XmlWriterSettings xstt = new XmlWriterSettings();
	xstt.ConformanceLevel = ConformanceLevel.Document;
	xstt.Encoding = useBOM ? Encoding.uTF8 : new uTF8Encoding(false);
	xstt.Indent = indent;
	xstt.IndentChars = new string(' ', indentation);
	xstt.NewLineChars = newlineChars;
	xstt.NewLineHandling = NewLineHandling.None;
	xstt.NewLineOnAttributes = false;
	xstt.OmitXmlDeclaration = false;
	return xstt;
}

Il metodo che costruisce i setting di formattazione per l’XmlWriter, tale oggetto viene poi passato alla creazione dell’ XmlWriter per informarlo di come vogliamo siano generati i dati XML scritti.

public static void SerializeToFile(string outputPath, object objToSerialize,  Type typeToSerialize, Type[] extraTypes = null, bool noNamespaces = false,
	string elementsPrefix = null, List<KeyValuePair<string, string>> namespaces = nullbool indent = true, int indentation = 4, string newlineChars = "\r\n", bool useBOM = true)
{
	try
	{

		using (XmlWriter writer = XmlWriter.Create(outputPath, GetWriterSettings(indent, indentation, newlineChars, useBOM)))
		{

			XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
			string prefix = elementsPrefix != null ? elementsPrefix : string.Empty;
			if (noNamespaces)
			{
				ns.Add(prefix, TXT_StandardNamespace);
			}
			else
			{
				if (namespaces != null)
				{
					foreach (KeyValuePair<string, string> nams in namespaces)
					{
						string prf = nams.Value == null ? string.Empty : nams.Value;
						ns.Add(prf, nams.Key);
					}

				}
			}

			XmlSerializer serializer = new XmlSerializer(typeToSerialize, extraTypes);
			serializer.Serialize(writer, objToSerialize, ns);
			writer.Close();
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il metodo per la serializzazione su file, invece che su stringa di un oggetto XML.

public static object DeserializeFromString(Type typeToDeserialize, string xmlString,  Type[] extraTypes = null)
{
	object ret = null;
	try
	{
		byte[] bites = new uTF8Encoding().GetBytes(xmlString);
		using (MemoryStream stream = new MemoryStream(bites))
		{

			XmlReader xr = XmlReader.Create(stream);

			XmlSerializer serializer = null;
			if (extraTypes != null)
			{
				serializer = new XmlSerializer(typeToDeserialize, extraTypes);
			}
			else
			{
				serializer = new XmlSerializer(typeToDeserialize);
			}
			ret = serializer.Deserialize(xr);
			xr.Close();
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
	return (ret);
}

Il metodo di deserializzazione di una stringa, come possiamo notare, al deserializzatore .Net non importa nulla della formattazione dei dati XML è abbastanza intelligente da verificare da solo come il file è fatto.

public static object DeserializeFromFile(Type typeToDeserialize,  string inputPath, Type[] extraTypes = null)
{
	object ret = null;
	try
	{
		using (XmlReader xr = XmlReader.Create(inputPath))
		{

			XmlSerializer serializer = null;
			if (extraTypes != null)
			{
				serializer = new XmlSerializer(typeToDeserialize, extraTypes);
			}
			else
			{
				serializer = new XmlSerializer(typeToDeserialize);
			}
			ret = serializer.Deserialize(xr);
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
	return (ret);
}

Il metodo di deserializzazione dati da file.

Il progetto di test

Come per i precedenti progetti di test per le classi di base, abbiamo creato una mini applicazione WPF che fornisce un interfaccia che permette di testare la creazione di una collection, la sua serializzazione e la sua deserializzazione.

test_settings_01[7]

Si tratta di una semplice finestra con due button che effettuano i test.

private void btnGenerate_Click(object sender, RoutedEventArgs e)
{
	DnwSettingsCollection settings = new DnwSettingsCollection();
	settings.Add( new DnwSetting()  { ID="STT_TXT",  Category="Test",
		Description="Setting di testo",
		EditorType= EditorType.TextBox,
		Value="Test Textbox Value" });
	settings.Add(new DnwSetting()
	{
		ID = "STT_COLOR",
		Category = "Test",
		Description = "Colore",
		EditorType = EditorType.TextBox,
		Value = "#FFFF0000"
	});
	settings.Add(new DnwSetting()
	{
		ID = "STT_CHECK",
		Category = "Test",
		Description = "Test CheckBox",
		EditorType = EditorType.CheckBox,
		Value = "True"
	});
	this.txtResult.Text = settings.WriteXml();
	settings.WriteXml(mFilename);
	this.txtStatus.Text = string.Format("File {0} created", mFilename);
}

Il bottone di generazione e serializzazione della collezione, Il dato salvato su file è quello mostrato dall’immagine seguente:

test_settings_02[7]

private void btnRead_Click(object sender, RoutedEventArgs e)
{
	DnwSettingsCollection settings =DnwSettingsCollection.ReadXml(mFilename,false);
	this.txtResult.Text = settings.ToString();
	this.txtStatus.Text = string.Format("File {0} read", mFilename);
}

Il bottone di deserializzazione, che rilegge i dati dal file appena salvato:

test_settings_03[7]

Il risultato della lettura del file salvato.

Conclusioni

In questo post piuttosto stringato, in quanto abbiamo inserito nelle librerie base oggetti già discussi in passato, abbiamo creato una classe e una collezione tipizzata di oggetti serializzabili, abbiamo inoltre implementato un Helper di serializzazione XML generico ed abbiamo testato il funzionamento dei metodi di serializzazione e deserializzazione generati in un piccolo progetto di test.

Il codice del progetto di test collegato a questo articolo può essere scaricato al link qui sotto:

Le librerie di uso comune ove sono state inserite le 2 classi discusse nell’articolo.

Per qualsiasi domanda, curiosità approfondimento, potete usare il link al modulo di contatto in cima alla pagina.