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
To samo pytanie co wyżej. Mam za zadanie dodać kolumnę do istniejącej tabeli łącząc obie inne kolumny ze sobą, ale nie mam pojęcia jak za to się zabrać
działa :) tylko była literówka :)
Podziękował. Trochę późno, po 8 latach, ale dzięki za testy (rozumiem że dla SQL2012 robione). Tak się właśnie zastanawiałem ile złego czynię stosując czasem __(max).
Super robota, korzystając z innych internetowych kalkulatorow po prostu wątpiłem w ich prawdomówność, w końcu trafiłem tutaj i wynik w końcu jest wiarygodny. 40 km w 2 h 810 kcal, ciekawostka: na fitatu wyliczyło mi 5700 kcal 😊 najlepiej będzie chyba jak kupię zegarek sportowy.
Wielkie dzieki za solidne wyjasnienia tematu.