Właściwy sposób na wdrożenie IXmlSerializable?

153

Gdy programista zdecyduje się na wdrożenie IXmlSerializable, jakie są zasady i najlepsze praktyki dotyczące jego wdrożenia? Słyszałem, że GetSchema()powinien powrócić nulli ReadXmlprzejść do następnego elementu przed powrotem. Czy to prawda? A co z WriteXml- czy powinien napisać element główny dla obiektu, czy zakłada się, że root jest już zapisany? Jak należy traktować i pisać przedmioty dziecięce?

Oto próbka tego, co mam teraz. Zaktualizuję go, gdy uzyskam dobre odpowiedzi.

public class MyCalendar : IXmlSerializable
{
    private string _name;
    private bool _enabled;
    private Color _color;
    private List<MyEvent> _events = new List<MyEvent>();


    public XmlSchema GetSchema() { return null; }

    public void ReadXml(XmlReader reader)
    {
        if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "MyCalendar")
        {
            _name    = reader["Name"];
            _enabled = Boolean.Parse(reader["Enabled"]);
            _color   = Color.FromArgb(Int32.Parse(reader["Color"]));

            if (reader.ReadToDescendant("MyEvent"))
            {
                while (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "MyEvent")
                {
                    MyEvent evt = new MyEvent();
                    evt.ReadXml(reader);
                    _events.Add(evt);
                }
            }
            reader.Read();
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("Name",    _name);
        writer.WriteAttributeString("Enabled", _enabled.ToString());
        writer.WriteAttributeString("Color",   _color.ToArgb().ToString());

        foreach (MyEvent evt in _events)
        {
            writer.WriteStartElement("MyEvent");
            evt.WriteXml(writer);
            writer.WriteEndElement();
        }
    }
}

public class MyEvent : IXmlSerializable
{
    private string _title;
    private DateTime _start;
    private DateTime _stop;


    public XmlSchema GetSchema() { return null; }

    public void ReadXml(XmlReader reader)
    {
        if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "MyEvent")
        {
            _title = reader["Title"];
            _start = DateTime.FromBinary(Int64.Parse(reader["Start"]));
            _stop  = DateTime.FromBinary(Int64.Parse(reader["Stop"]));
            reader.Read();
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("Title", _title);
        writer.WriteAttributeString("Start", _start.ToBinary().ToString());
        writer.WriteAttributeString("Stop",  _stop.ToBinary().ToString());
    }
}

Odpowiedni przykładowy XML

<MyCalendar Name="Master Plan" Enabled="True" Color="-14069085">
    <MyEvent Title="Write Code" Start="-8589241828854775808" Stop="-8589241756854775808" />
    <MyEvent Title="???" Start="-8589241828854775808" Stop="-8589241756854775808" />
    <MyEvent Title="Profit!" Start="-8589247048854775808" Stop="-8589246976854775808" />
</MyCalendar>
Greg
źródło
3
Czy możesz dodać próbkę XML do tego pytania? Ułatwiłoby to czytanie wraz z kodem. Dzięki!
Rory
A co z sytuacją, w której po ostatnim zdarzeniu w pliku xml znajduje się komentarz XML itp. tj. czy powinieneś zakończyć metodę ReadXml () czymś, co sprawdza, czy przeczytałeś do elementu końcowego? Obecnie zakłada się, że ostatnia Read () robi to, ale może nie zawsze.
Rory
7
@Rory - dodano próbkę. Lepiej późno niż wcale?
Greg,
@Greg Dobre informacje. Czy nie chciałbyś, aby ReadXml i WriteXml używały niezmiennej kultury? Myślę, że możesz napotkać problemy, jeśli użytkownik przeniósł się do innego kraju i zmienił ustawienia regionu i języka. W takim przypadku kod może nie zostać poprawnie zdeserializowany. Czytałem, że najlepszą praktyką jest zawsze używanie niezmiennej kultury podczas serializacji
publiczne bezprzewodowe

Odpowiedzi:

100

Tak, GetSchema () powinna zwrócić wartość null .

Metoda IXmlSerializable.GetSchema Ta metoda jest zarezerwowana i nie powinna być używana. Podczas implementowania interfejsu IXmlSerializable należy zwrócić odwołanie o wartości null (nic w języku Visual Basic) z tej metody, a zamiast tego, jeśli jest wymagane określenie niestandardowego schematu, zastosuj XmlSchemaProviderAttribute do klasy.

Zarówno dla odczytu, jak i zapisu element object został już zapisany, więc nie ma potrzeby dodawania elementu zewnętrznego podczas zapisu. Na przykład możesz po prostu zacząć czytać / zapisywać atrybuty w obu.

Do napisania :

Podana implementacja WriteXml powinna wypisać reprezentację XML obiektu. Struktura zapisuje element opakowania i umieszcza moduł zapisujący XML po jego uruchomieniu. Twoja implementacja może napisać swoją zawartość, w tym elementy podrzędne. Struktura następnie zamyka element opakowania.

I do przeczytania :

Metoda ReadXml musi odtworzyć obiekt przy użyciu informacji zapisanych przez metodę WriteXml.

Gdy ta metoda jest wywoływana, czytnik jest umieszczany na początku elementu, który zawija informacje dla Twojego typu. Oznacza to, że tuż przed znacznikiem początkowym, który wskazuje początek serializowanego obiektu. Kiedy ta metoda zwraca, musi odczytać cały element od początku do końca, łącznie z całą jego zawartością. W przeciwieństwie do metody WriteXml platforma nie obsługuje automatycznie elementu opakowania. Twoja implementacja musi to zrobić. Niezastosowanie się do tych reguł pozycjonowania może spowodować wygenerowanie przez kod nieoczekiwanych wyjątków w czasie wykonywania lub uszkodzenie danych.

Zgadzam się, że jest to trochę niejasne, ale sprowadza się to do stwierdzenia: „Twoja praca polega na Read()tagu elementu końcowego opakowania”.

Marc Gravell
źródło
A co z pisaniem i czytaniem elementów wydarzenia? Ręczne pisanie elementu początkowego wydaje się hackem. Myślę, że widziałem, jak ktoś używa XmlSerializer w metodzie write do napisania każdego elementu podrzędnego.
Greg
@Greg; każde użycie jest w porządku ... tak, możesz użyć zagnieżdżonego XmlSerializera, jeśli potrzebujesz, ale nie jest to jedyna opcja.
Marc Gravell
3
Dzięki za te szczegóły, przykładowy kod w witrynie MSDN jest dość bezużyteczny i niejasny. Wiele razy utknąłem i zastanawiałem się nad asymetrycznym zachowaniem Read / WriteXml.
jdehaan
1
@MarcGravell Wiem, że to stary wątek. „Framework zapisuje element opakowania i umieszcza moduł zapisujący XML po jego uruchomieniu”. Tutaj walczę. Czy istnieje sposób, aby zmusić platformę do pominięcia tego etapu automatycznej obsługi opakowania? Mam sytuację, w której muszę pominąć ten krok: stackoverflow.com/questions/20885455/…
James
@James nie według mojej najlepszej wiedzy
Marc Gravell
34

Napisałem jeden artykuł na ten temat z przykładami, ponieważ dokumentacja MSDN jest obecnie dość niejasna, a przykłady, które można znaleźć w Internecie, są w większości przypadków nieprawidłowo zaimplementowane.

Pułapki dotyczą obsługi lokalizacji i pustych elementów poza tym, o czym wspomniał już Marc Gravell.

http://www.codeproject.com/KB/XML/ImplementIXmlSerializable.aspx

jdehaan
źródło
Świetny artykuł! Na pewno będę się do tego odwoływał następnym razem, gdy będę chciał serializować niektóre dane.
Greg
Dzięki! ilość pozytywnych opinii wynagradza czas poświęcony na ich napisanie. Głęboko doceniam, że Ci się podoba! Nie wahaj się poprosić o krytykę niektórych punktów.
jdehaan
Przykłady są znacznie bardziej przydatne niż cytowanie MSDN.
Dzięki za projekt kodu, zagłosowałbym też za tym, gdybym mógł. Informacje na temat atrybutów były całkowicie obszerne w porównaniu z MSDN. Na przykład klasa my: IXMLSerializable zepsuła się, gdy została poprzedzona prefiksem przez wygenerowany plik xsd.exe [Serializable (), XmlType (Namespace = "MonitorService")].
John,
8

Tak, to całe pole minowe, prawda? Odpowiedź Marca Gravella prawie to pokrywa, ale chciałbym dodać, że w projekcie, nad którym pracowałem, okazało się, że ręczne pisanie zewnętrznego elementu XML jest dość niewygodne. Spowodowało to również niespójne nazwy elementów XML dla obiektów tego samego typu.

Naszym rozwiązaniem było zdefiniowanie własnego IXmlSerializableinterfejsu, wywodzącego się z systemowego, do którego dodano metodę o nazwie WriteOuterXml(). Jak można się domyślić, metoda ta po prostu zapisuje element zewnętrzny, następnie wywołuje WriteXml(), a następnie zapisuje koniec elementu. Oczywiście systemowy serializator XML nie wywoła tej metody, więc był on przydatny tylko wtedy, gdy wykonaliśmy własną serializację, więc może to być pomocne lub nie w twoim przypadku. Podobnie dodaliśmy ReadContentXml()metodę, która nie czytała zewnętrznego elementu, tylko jego zawartość.

EMP
źródło
5
W C # 3.0 prawdopodobnie możesz to zrobić, pisząc zamiast tego metodę rozszerzającą, ale jest to ciekawy pomysł.
Marc Gravell
2

Jeśli masz już reprezentację klasy XmlDocument lub wolisz sposób pracy ze strukturami XML w XmlDocument, szybkim i brudnym sposobem implementacji IXmlSerializable jest po prostu przekazanie tego xmldoc do różnych funkcji.

OSTRZEŻENIE: XmlDocument (i / lub XDocument) jest o rząd wielkości wolniejszy niż xmlreader / writer, więc jeśli wydajność jest absolutnym wymogiem, to rozwiązanie nie jest dla Ciebie!

class ExampleBaseClass : IXmlSerializable { 
    public XmlDocument xmlDocument { get; set; }
    public XmlSchema GetSchema()
    {
        return null;
    }
    public void ReadXml(XmlReader reader)
    {
        xmlDocument.Load(reader);
    }

    public void WriteXml(XmlWriter writer)
    {
        xmlDocument.WriteTo(writer);
    }
}
Thijs Dalhuijsen
źródło
0

Implementacja interfejsu jest opisana w innych odpowiedziach, ale chciałem dorzucić moje 2 centy za element główny.

W przeszłości nauczyłem się, że wolę umieszczać element główny jako metadane. Ma to kilka zalet:

  • Jeśli istnieje obiekt o wartości null, nadal można go serializować
  • Z punktu widzenia czytelności kodu ma to sens

Poniżej znajduje się przykład słownika z możliwością serializacji, w którym element główny słownika jest zdefiniowany w następujący sposób:

using System.Collections.Generic;

[System.Xml.Serialization.XmlRoot("dictionary")]
public partial class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, System.Xml.Serialization.IXmlSerializable
{
            public virtual System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public virtual void ReadXml(System.Xml.XmlReader reader)
    {
        var keySerializer = new System.Xml.Serialization.XmlSerializer(typeof(TKey));
        var valueSerializer = new System.Xml.Serialization.XmlSerializer(typeof(TValue));
        bool wasEmpty = reader.IsEmptyElement;
        reader.Read();
        if (wasEmpty)
            return;
        while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
        {
            reader.ReadStartElement("item");
            reader.ReadStartElement("key");
            TKey key = (TKey)keySerializer.Deserialize(reader);
            reader.ReadEndElement();
            reader.ReadStartElement("value");
            TValue value = (TValue)valueSerializer.Deserialize(reader);
            reader.ReadEndElement();
            Add(key, value);
            reader.ReadEndElement();
            reader.MoveToContent();
        }

        reader.ReadEndElement();
    }

    public virtual void WriteXml(System.Xml.XmlWriter writer)
    {
        var keySerializer = new System.Xml.Serialization.XmlSerializer(typeof(TKey));
        var valueSerializer = new System.Xml.Serialization.XmlSerializer(typeof(TValue));
        foreach (TKey key in Keys)
        {
            writer.WriteStartElement("item");
            writer.WriteStartElement("key");
            keySerializer.Serialize(writer, key);
            writer.WriteEndElement();
            writer.WriteStartElement("value");
            var value = this[key];
            valueSerializer.Serialize(writer, value);
            writer.WriteEndElement();
            writer.WriteEndElement();
        }
    }

    public SerializableDictionary() : base()
    {
    }

    public SerializableDictionary(IDictionary<TKey, TValue> dictionary) : base(dictionary)
    {
    }

    public SerializableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) : base(dictionary, comparer)
    {
    }

    public SerializableDictionary(IEqualityComparer<TKey> comparer) : base(comparer)
    {
    }

    public SerializableDictionary(int capacity) : base(capacity)
    {
    }

    public SerializableDictionary(int capacity, IEqualityComparer<TKey> comparer) : base(capacity, comparer)
    {
    }

}
VoteCoffee
źródło