Introduzione
La serializzazione è un
processo tramite il quale trasformiamo l'istanza di una classe in un formato
trasportabile. Il formato può essere uno stream di tipo XML o di tipo binario,
dipendentemente dal tipo di serializzazione che intendiamo ottenere.
Il formato XML ha diversi vantaggi:
innanzitutto è human-readable, quindi può essere facilemente essere letto con un
qualsiasi editor di testi (più o meno specializzato per il trattamento di
documenti XML). In secondo luogo, uno stream XML può facilmente essere
trasmesso ad un web-services via http, dato che non contiene caratteri speciali
che potrebbero essere filtrati o bloccati da regole impostate in un eventuale
firewall.
Il formato binario invece, come
dice il nome stesso, serializza l'istanza di classe in un più generico stream di
bytes. E' un formato più compatto rispetto alla serializzazione XML. Se
decidiamo di serializzare in formato binario abbiamo un grande vantaggio:
possiamo intercettare la richiesta di serializzazione
della classe ed intervenire, customizzando completamente il modo con cui il FX
serializzerebbe di default. D'altro canto, il formato binario per sua natura
potrebbe contenere caratteri anomali(passatemi il termine) che
potrebbe venir bloccato da eventuali firewall: questo lo rende poco adatto
alla trasmissione http.
Serializzazione in formato XML
La serializzazione più
semplice in assoluto passa attraverso la classe XmlSerializer del FX. Nel codice che ho messo a disposizione c'è
già una classe XMLSerialization con tutta una serie di metodi statici che
serializzano le classi coinvolte dal progetto (Storage, Shelf, Book e Chapter):
vi rimando a quel codice per vedere ed esaminare concretamente il funzionamento.
La cosa importante da sapere è che ogni classe .NET serializza soltanto le
proprietà pubbliche di cui dispone. Se va bene il comportamento di default che
ci offre il FX, possiamo portare a termine la serializzazione XML in modo molto
semplice in questo modo:
static public void Serialize(Chapter cp, string fileName)
{
if (regole == null)
SetupWriterSettings();
XmlWriter wri = XmlWriter.Create(fileName, regole);
XmlSerializer ser = new XmlSerializer(typeof(Chapter));
ser.Serialize(wri, cp);
wri.Close();
}
La classe XmlWriter del FX ci permette di ottenere un oggetto con il quale
gestire uno stream XML: nell'esempio qui sopra, lo usiamo per scrivere
fisicamente un file su disco. La chiamata a SetupWriterSettings() inizializza
un'istanza della classe XmlWriterSettings, un oggetto che uso spesso per definire alcune
proprietà relative al modo con cui il FX genererà lo stream (indentazione,
encoding, creare una nuova linea sugli attributi, etc.). Il codice che
inizializza il mio oggetto regole è questo:
private static void SetupWriterSettings()
{
regole = new XmlWriterSettings();
regole.Indent = true;
regole.IndentChars = " ";
regole.ConformanceLevel = ConformanceLevel.Auto;
regole.NewLineOnAttributes = false;
regole.Encoding = System.Text.Encoding.Unicode;
}
Successivamente, istanzio un XmlSerializer, indicando nel suo
costruttore la classe che sto serializzando. Il processo vero e proprio viene
portato a termine dalla linea:
ser.Serialize(wri, cp);
Questa riga serializza l'oggetto cp (Chapter) nello
stream indicato da wri (XmlWriter). Nulla di più, nulla di
meno. Ovviamente, a questo punto possiamo andare a dare un'occhiata al file
generato, aprirlo con un editor qualsiasi e dare un'occhiata a quanto generato.
Ad esempio, un'istanza di classe Chapter viene resa nel seguente formato
XML:
<?xml version="1.0" encoding="utf-16"?>
<Chapter
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Title>Prologo</Title>
</Chapter>
Ovviamente, oggetti più complessi creano un flusso XML più complesso. Ecco un
esempio dello stream XML generato da un'istanza della classe Book.
<?xml version="1.0" encoding="utf-16"?>
<Book
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Title>La Spada di Shannara</Title>
<Pages>0</Pages>
<InLoan>false</InLoan>
<Bookmark>0</Bookmark>
<Chapters>
<Chapter>
<Title>Prologo</Title>
</Chapter>
<Chapter>
<Title>Inizio</Title>
</Chapter>
</Chapters>
</Book>
Nel suo comportamento di default, il FX serializza tutte le public
properties della classe. Tramite una serie di attributi possiamo decorare ogni
property per modificare questo comportamento. Ad esempio, possiamo stabilire
che la property Author della classe Book
venga serializzata con il tag <Autore> (XmlElementAttribute). Oppure, ancora,
possiamo dire di ignorare una certa property con l'attributo XmlIgnoreAttribute. Vi consiglio una lettura veloce al documento
"Attributes That Control XML Serialization" per maggiori
dettagli sugli attributi disponibili nel FX a questo scopo.
Serializzazione binaria
La
serializzazione binaria viene effettuata tramite l'utilizzo della classe BinaryFormatter nel namespace System.Runtime.Serialization.Formatters.Binary (lo preciso perchè ci
ho messo una vita prima di capire dove trovarla - viva l'Object Browser, alias
CTRL W + J ).
static public void SerializeBinary(Chapter cp, string fileName)
{
FileStream fs = new FileStream(fileName, FileMode.CreateNew);
BinaryFormatter form = new BinaryFormatter();
form.Serialize(fs, cp);
fs.Close();
fs.Dispose();
}
Questa volta lo stream fs è creato attraverso la classe
FileStream, mentre la classe BinaryFormatter compie realmente l'operazione di
serializzazione dell'oggetto cp. Il file generato non è
human-readable, è più compatto del corrispondente file XML che abbiamo visto
prima. Anche in questo caso, però, contiene a tutti gli effetti tutte le
informazioni necessarie al FX per deserializzare, e quindi per ricostruire
l'istanza dell'oggetto in memoria.
Implementare l'interfaccia
ISerializable
Nei processi di serializzazione mostrati qui sopra,
non abbiamo alcuna possibilità di intervenire sul processo stesso. Il FX ci
viene in aiuto, ha lui il compito di serializzare e a noi va bene così,
soprattutto nella maggior parte dei casi. Se per qualsiasi motivo vogliamo
modificare questo comportamento, possiamo utilizzare l'interfaccia ISerializable. Questa interfaccia richiede l'implementazione di un
solo metodo GetObjectData(), che viene chiamata automaticamente dal
FX quando viene richiesta la serializzazione della classe stessa. Vediamo nel
dettaglio come funziona questa cosa.
using System;
[Serializable]
public class Chapter : ISerializable
{
// codice, codice, codice
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
}
}
Notare che:
- ho dovuto decorare l'intera classe con l'attributo Serializable (namespace System).
- ho dovuto ovviamente indicare che Chapter implementa ISerializable
- in conseguenza al punto (2), ho dovuto implementare
GetObjectData
Il metodo GetObjectData() viene chiamata al momento della serializzazione
binaria della classe (non viene
usato se serializziamo con XML). In questo modo agiamo sull'oggetto
info (classe SerializationInfo) aggiungendo tutte le informazioni che
intendiamo serializzare sullo stream tramite la chiamata al metodo AddValue (il quale dispone di
16 overload diversi per ogni value-type)
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Title", _Title);
info.AddValue("CurrentDate", DateTime.Now.ToShortDateString());
string user = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
info.AddValue("CurrentUser", user);
}
Nel codice qui sopra, ad esempio, oltre a serializzare la property
Title (che avrebbe fatto anche il FX di default), aggiungiamo
la data corrente e l'utente Windows corrente che sta compiendo questa
operazione. Tutto quello che andiamo ad aggiungere ad info
verrà a tutti gli effetti serializzato sullo stream.