Richtige Implementierung von IXmlSerializable?

153

IXmlSerializableWelche Regeln und Best Practices gibt ein Programmierer , wenn er sich für die Implementierung entscheidet? Ich habe gehört, dass dies GetSchema()zurückkehren nullund ReadXmlvor der Rückkehr zum nächsten Element wechseln sollte. Ist das wahr? Und was ist mit WriteXml- sollte es ein Wurzelelement für das Objekt schreiben oder wird angenommen, dass die Wurzel bereits geschrieben ist? Wie sollen untergeordnete Objekte behandelt und geschrieben werden?

Hier ist ein Beispiel von dem, was ich jetzt habe. Ich werde es aktualisieren, sobald ich gute Antworten bekomme.

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());
    }
}

Entsprechendes Beispiel-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
quelle
3
Könnten Sie dieser Frage ein XML-Beispiel hinzufügen? Dies würde das Lesen zusammen mit dem Code vereinfachen. Vielen Dank!
Rory
Was ist mit dem Fall, in dem nach dem letzten Ereignis in Ihrer XML ein XML-Kommentar usw. vorhanden ist? dh sollten Sie die ReadXml () -Methode mit etwas beenden, das überprüft, ob Sie bis zum Endelement durchgelesen haben? Derzeit wird davon ausgegangen, dass das letzte Read () dies tut, dies jedoch möglicherweise nicht immer.
Rory
7
@Rory - Beispiel hinzugefügt. Besser spät als nie?
Greg
@ Greg Gute Infos. Möchten Sie nicht auch, dass ReadXml und WriteXml die invariante Kultur verwenden? Ich denke, Sie könnten auf Probleme stoßen, wenn der Benutzer in ein anderes Land gezogen ist und seine Regions- und Spracheinstellungen geändert hat. In diesem Fall wird der Code möglicherweise nicht korrekt deserialisiert. Ich habe gelesen, dass es eine bewährte Methode ist, bei der Serialisierung immer die invariante Kultur zu verwenden
Public Wireless

Antworten:

100

Ja, GetSchema () sollte null zurückgeben .

IXmlSerializable.GetSchema-Methode Diese Methode ist reserviert und sollte nicht verwendet werden. Wenn Sie die IXmlSerializable-Schnittstelle implementieren, sollten Sie eine Nullreferenz (Nothing in Visual Basic) von dieser Methode zurückgeben. Wenn stattdessen ein benutzerdefiniertes Schema angegeben werden soll, wenden Sie das XmlSchemaProviderAttribute auf die Klasse an.

Sowohl beim Lesen als auch beim Schreiben wurde das Objektelement bereits geschrieben, sodass Sie beim Schreiben kein äußeres Element hinzufügen müssen. Zum Beispiel können Sie einfach mit dem Lesen / Schreiben von Attributen in beiden beginnen.

Zum Schreiben :

Die von Ihnen bereitgestellte WriteXml-Implementierung sollte die XML-Darstellung des Objekts ausschreiben. Das Framework schreibt ein Wrapper-Element und positioniert den XML-Writer nach seinem Start. Ihre Implementierung kann den Inhalt einschließlich untergeordneter Elemente schreiben. Das Framework schließt dann das Wrapper-Element.

Und zum Lesen :

Die ReadXml-Methode muss Ihr Objekt anhand der Informationen wiederherstellen, die von der WriteXml-Methode geschrieben wurden.

Wenn diese Methode aufgerufen wird, befindet sich der Reader am Anfang des Elements, das die Informationen für Ihren Typ umschließt. Das heißt, kurz vor dem Start-Tag, das den Beginn eines serialisierten Objekts angibt. Wenn diese Methode zurückgegeben wird, muss sie das gesamte Element von Anfang bis Ende gelesen haben, einschließlich des gesamten Inhalts. Im Gegensatz zur WriteXml-Methode behandelt das Framework das Wrapper-Element nicht automatisch. Ihre Implementierung muss dies tun. Die Nichtbeachtung dieser Positionierungsregeln kann dazu führen, dass Code unerwartete Laufzeitausnahmen oder beschädigte Daten generiert.

Ich bin damit einverstanden, dass dies ein wenig unklar ist, aber es läuft darauf hinaus, "es ist Ihre Aufgabe, Read()das Endelement-Tag des Wrappers zu erstellen".

Marc Gravell
quelle
Was ist mit dem Schreiben und Lesen der Event-Elemente? Es fühlt sich hackisch an, das Startelement manuell zu schreiben. Ich glaube, ich habe jemanden gesehen, der einen XmlSerializer in der Schreibmethode verwendet, um jedes untergeordnete Element zu schreiben.
Greg
@ Greg; Beide Verwendungszwecke sind in Ordnung ... Ja, Sie können bei Bedarf einen verschachtelten XmlSerializer verwenden, dies ist jedoch nicht die einzige Option.
Marc Gravell
3
Vielen Dank für diese Präzision. Der Beispielcode in MSDN ist diesbezüglich ziemlich nutzlos und unklar. Ich blieb viele Male stecken und wunderte mich über das asymmetrische Verhalten von Read / WriteXml.
Jdehaan
1
@MarcGravell Ich weiß, dass dies ein alter Thread ist. "Das Framework schreibt ein Wrapper-Element und positioniert den XML-Writer nach seinem Start." Hier kämpfe ich. Gibt es eine Möglichkeit, das Framework zu zwingen, diesen Schritt der automatischen Behandlung des Wrappers zu überspringen? Ich habe eine Situation, in der ich diesen Schritt überspringen muss: stackoverflow.com/questions/20885455/…
James
@ James nicht nach bestem Wissen
Marc Gravell
34

Ich habe einen Artikel zu diesem Thema mit Beispielen geschrieben, da die MSDN-Dokumentation inzwischen ziemlich unklar ist und die Beispiele, die Sie im Internet finden, die meiste Zeit falsch implementiert sind.

Fallstricke sind der Umgang mit Gebietsschemas und leeren Elementen neben dem, was Marc Gravell bereits erwähnt hat.

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

jdehaan
quelle
Ausgezeichneter Artikel! Ich werde auf jeden Fall darauf verweisen, wenn ich das nächste Mal Daten serialisieren möchte.
Greg
Vielen Dank! Die Menge an positivem Feedback belohnt die Zeit, die für das Schreiben aufgewendet wurde. Ich schätze es sehr, dass es Ihnen gefällt! Zögern Sie nicht, einige Punkte zu kritisieren.
Jdehaan
Beispiele sind weitaus nützlicher als das Zitieren von MSDN.
Danke für das Codeprojekt, ich würde das auch abstimmen, wenn ich könnte. Das Zeug zu Attributen war im Vergleich zu MSDN äußerst umfassend. Zum Beispiel ist meine: IXMLSerializable-Klasse beim Präfixieren der generierten xsd.exe [Serializable (), XmlType (Namespace = "MonitorService")] fehlerhaft.
John
8

Ja, das Ganze ist ein bisschen wie ein Minenfeld, nicht wahr? Marc Gravells Antwort deckt es ziemlich genau ab, aber ich möchte hinzufügen, dass es in einem Projekt, an dem ich gearbeitet habe, ziemlich umständlich war, das äußere XML-Element manuell schreiben zu müssen. Dies führte auch zu inkonsistenten XML-Elementnamen für Objekte desselben Typs.

Unsere Lösung bestand darin, eine eigene IXmlSerializableSchnittstelle zu definieren , die vom System abgeleitet wurde und eine Methode namens hinzufügte WriteOuterXml(). Wie Sie sich vorstellen können, würde diese Methode einfach das äußere Element schreiben, dann aufrufen WriteXml()und dann das Ende des Elements schreiben. Natürlich würde der System-XML-Serializer diese Methode nicht aufrufen, daher war sie nur nützlich, wenn wir unsere eigene Serialisierung durchgeführt haben, sodass dies in Ihrem Fall möglicherweise hilfreich ist oder nicht. In ähnlicher Weise haben wir eine ReadContentXml()Methode hinzugefügt , die nicht das äußere Element, sondern nur dessen Inhalt gelesen hat.

EMP
quelle
5
Mit C # 3.0 können Sie dies wahrscheinlich tun, indem Sie stattdessen eine Erweiterungsmethode schreiben, aber eine interessante Idee.
Marc Gravell
2

Wenn Sie bereits eine XmlDocument-Darstellung Ihrer Klasse haben oder die XmlDocument-Methode zum Arbeiten mit XML-Strukturen bevorzugen, können Sie IXmlSerializable schnell und unkompliziert implementieren, indem Sie dieses xmldoc einfach an die verschiedenen Funktionen übergeben.

WARNUNG: XmlDocument (und / oder XDocument) ist um eine Größenordnung langsamer als xmlreader / writer. Wenn also Leistung unbedingt erforderlich ist, ist diese Lösung nichts für Sie!

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
quelle
0

Die Schnittstellenimplementierung wird durch die anderen Antworten abgedeckt, aber ich wollte meine 2 Cent für das Stammelement einwerfen.

Ich habe in der Vergangenheit gelernt, das Stammelement lieber als Metadaten zu verwenden. Dies hat einige Vorteile:

  • Wenn ein Nullobjekt vorhanden ist, kann es trotzdem serialisiert werden
  • Unter dem Gesichtspunkt der Lesbarkeit von Code ist dies sinnvoll

Unten finden Sie ein Beispiel für ein serialisierbares Wörterbuch, in dem das Wörterbuchstammelement auf diese Weise definiert ist:

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
quelle