Spis treści:

Kategoria:C#Serializacja


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:

<Width>6.5in</Width>
<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 class ReportSize
{
    public Units Unit { getset; }
    public decimal Value { getset; }

    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 size = new ReportSize(29.7M);
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ś:

<?xml version="1.0encoding="utf-16"?>
<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:

public class ReportSizeIXmlSerializable
{
    // 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:

public enum Units
{
    // 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:

<?xml version="1.0encoding="utf-16"?>
<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

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za rozjasnienie zagadnienia upsert. wlasnie sie ucze programowania :).
Co się stanie gdy spróbuję wyszukać:
SELECT * FROM NV_Airport WHERE Code='SVO'
SELECT * FROM V_Airport WHERE Code=N'SVO'
(odwrotnie są te N-ki)
Będzie konwersja czy nie znajdzie żadnego rekordu?