Własny format serializacji wartości
Znów o serializacji
Serializację jakiegoś gatunku można znaleźć prawie wszędzie. Niedawno pisałem o serializacji WCF (zobacz tu: Serializacja w WCF od środka) i nie przypuszczałem, że temat powróci tak szybko.
Temat wrócił przy okazji impementacji formatu RDLFormat wykorzystywany do tworzenia raportów w Visual Studio, SQL Server Reporting Services i Crystal Reports. Z tej rodziny pochodzi także kliencka wersja formatu - RDLC. (ang. Report Definition Language) i wykorzystywanego przez ten format zapisu jednostek:
<Page>
<PageHeight>29.7cm</PageHeight>
<PageWidth>21cm</PageWidth>
<Style />
</Page>
Problemem tradycyjnej serializacji może być w tym przypadku jednostka. Istnieje oczywiście możliwość traktowania tego ciągu jako łańcucha znaków i użycia typu string, ale ciągłe konwersje i przeliczenia mogą być przyczyną błędów, a już na pewno zmniejszą czytelność kodu. Jak sprawić, aby nasze pole było reprezentowane przez zwykłą klasę z dwoma atrybutami: wartością typu decimal i jednostkę typu wyliczeniowego? Tradycyjny sposób serializacji z dwóch właściwości zrobi nam dwa elementy XML. Nie chcemy tego, więc musimy nieco wniknąć w sposób serializacji klasy XmlSerializer.
Dwie właściwości .NET, jeden element XML
Naszym poligonem doświadczalnym będzie prosta klasa reprezentująca wymiary w różnych jednostkach:
{
public Units Unit { get; set; }
public decimal Value { get; set; }
public ReportSize()
{
Unit = Units.cm;
}
public ReportSize(decimal valueInCm)
: this()
{
this.Value = valueInCm;
}
public ReportSize(decimal value, Units unit)
{
this.Value = value;
this.Unit = unit;
}
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0}{1}", Value, Unit);
}
}
Serializacja takiego obiektu wyglądałaby mniej więcej tak:
var serializer = new XmlSerializer(typeof(ReportSize));
using (var writer = new StringWriter())
{
serializer.Serialize(writer, size);
Console.WriteLine(writer.ToString());
}
Jako efekt końcowy serializacji uzyskalibyśmy takie coś:
<ReportSize xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Unit>cm</Unit>
<Value>29.7</Value>
</ReportSize>
Pomijam w tym momencie przestrzenie nazw, bo nie o nich mowa. Chodzi o to, że mamy element główny oraz dwa podelementy: Unit oraz Value. Chcemy to wszystko skumulować w jednym.
Przejmujemy pełną kontrolę - IXmlSerializable
Istnieje wiele różnych możliwości wpływania na sposób serializacji obiektów w .NET. Najpowszechniejszym i najpopularniejszym sposobem jest udekorowanie różnych właściwości odpowiednimi atrybutami. Najtrudniejszym, ale przy okazji najbardziej elastycznym rozwiązaniem jest wykonanie serializacji własnoręcznie. Aby klasa XmlSerializer wiedziała o naszych zamiarach, musimy zaimplementować interfejs ISerializable. Kluczowe są w nim dwie metody: serializacji i deserializacji. Interfejs ma jeszcze trzecią metodę, GetSchema, służącą do przekazania schematu, ale można ją w praktyce pominąć i zapomnieć o niej. Popatrzmy na przykładową implementację wspomnianego interfejsu:
{
// Pozostała część klasy
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(System.Xml.XmlReader reader)
{
string value = reader.ReadInnerXml();
int split = value.IndexOfAny(Enum.GetNames(typeof(Units)).Select(a => a[0]).ToArray());
Value = decimal.Parse(value.Substring(0, split), CultureInfo.InvariantCulture);
Unit = (Units)Enum.Parse(typeof(Units), value.Substring(split));
}
public void WriteXml(System.Xml.XmlWriter writer)
{
writer.WriteValue(this.ToString());
}
}
Gdy klasa XmlSerializer będzie próbowała przetworzyć naszą klasę i dostrzeże implementację interfejsu IXmlSerializable, zamiast wykorzystywać mechanizmy domyślne - wywoła jedną z dwóch metod:
- ReadXml - podczas deserializacji,
- WriteXml - podczas serializacji.
Implementacja zapisu będzie bajecznie prosta i sprowadzi się do wywołania metody ToString. Należy jednak mieć na uwadze różny sposób formatowania liczb, a w szczególności wartość separatora oddzielającego część całkowitą od części ułamkowej. Przyjęło się, że w Polsce używa się do tego przecinka. Część krajów używa kropki. Wszystko to zależy od ustawień regionalnych i językowych systemu operacyjnego. Aby mieć pewność, że format będzie zawsze taki sam, należy o to zadbać. Ja wykorzystałem ustawienia kultury ogólnej - InvariantCulture.
Odczyt jest już trochę bardziej skomplikowany. Po pierwsze, wczytuję całe wnętrze elementu - wiemy, że wszystkie atrybuty zostały spłaszczone do postaci łańcucha znaków i taki też łańcuch będzie tym wnętrzem. Po drugie, należy odnaleźć miejsce, w którym kończy się część liczbowa, a zaczyna symbol jednostki. Ja zastosowałem rozwiązanie niezbyt wydajne, ale krótkie w zapisie. Szukam pierwszego wystąpienia pierwszej litery z nazw typu wyliczeniowego. To tam będzie granica podziału. Pierwszą część konwertuję na liczbę typu decimal, z uwzględnieniem kultury, drugą część zamieniam na typ wyliczeniowy. Typ wyliczeniowy mógłby być zdefiniowany następująco:
{
// Centymetry
cm,
//Milimetry
mm,
//Cale
@in
}
Przy okazji przypomnę, że in jest słowem kluczowym i nie można tak nazwać pola. Aby użyć w nazwie słowa zarezerwowanego należy takie słowo poprzedzić symbolem @.
Końcowy wynik serializacji
Tak przygotowaną klasę można bez problemów poddać serializacji i cieszyć się oczekiwanym efektem:
<ReportSize>29.7cm</ReportSize>
Otrzymaliśmy piękną i zwięzłą postać serializacji. Powiem nawet więcej: otrzymaliśmy postać serializacji zgodną ze specyfikacją formatu raportowego RDL! O to w zasadzie chodziło.
Kategoria:C#Serializacja
Brak komentarzy - bądź pierwszy