Modo corretto di implementare IXmlSerializable?


153

Una volta che un programmatore decide di implementare IXmlSerializable, quali sono le regole e le migliori pratiche per implementarlo? Ho sentito che GetSchema()dovrebbe tornare nulle ReadXmldovrebbe passare all'elemento successivo prima di tornare. È vero? E che dire WriteXml- dovrebbe scrivere un elemento radice per l'oggetto o si presume che la radice sia già scritta? Come devono essere trattati e scritti gli oggetti figlio?

Ecco un esempio di quello che ho ora. Lo aggiornerò man mano che ottengo buone risposte.

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

XML di esempio corrispondente

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

3
Potresti aggiungere un campione XML a questa domanda? Renderebbe più semplice la lettura insieme al codice. Grazie!
Rory,

Che ne dici di affrontare il caso in cui c'è un commento XML ecc. Dopo l'ultimo Evento nel tuo XML. cioè dovresti finire il metodo ReadXml () con qualcosa che controlla di aver letto fino all'elemento finale? Attualmente questo presuppone che l'ultimo Read () lo faccia ma potrebbe non essere sempre.
Rory,

7
@Rory - Esempio aggiunto. Meglio tardi che mai?
Greg,

@Greg Buone informazioni. Non vorresti che ReadXml e WriteXml utilizzassero anche Invariant Culture? Penso che potresti incorrere in problemi se l'utente si trasferisse in un altro Paese e modificasse le impostazioni di Regione e Lingua. In tal caso il codice potrebbe non deserializzare correttamente. Ho letto che è consigliabile utilizzare sempre Invariant Culture quando si esegue la serializzazione
wireless pubblico

Risposte:


100

Sì, GetSchema () dovrebbe restituire null .

Metodo IXmlSerializable.GetSchema Questo metodo è riservato e non deve essere utilizzato. Quando si implementa l'interfaccia IXmlSerializable, è necessario restituire un riferimento null (Nothing in Visual Basic) da questo metodo e, invece, se è necessario specificare uno schema personalizzato, applicare XmlSchemaProviderAttribute alla classe.

Sia in lettura che in scrittura, l'elemento oggetto è già stato scritto, quindi non è necessario aggiungere un elemento esterno in scrittura. Ad esempio, puoi semplicemente iniziare a leggere / scrivere gli attributi nei due.

Per scrivere :

L'implementazione WriteXml fornita deve scrivere la rappresentazione XML dell'oggetto. Il framework scrive un elemento wrapper e posiziona il writer XML dopo il suo avvio. L'implementazione può scrivere i suoi contenuti, inclusi gli elementi figlio. Il framework quindi chiude l'elemento wrapper.

E per leggere :

Il metodo ReadXml deve ricostituire il tuo oggetto utilizzando le informazioni scritte dal metodo WriteXml.

Quando viene chiamato questo metodo, il lettore viene posizionato all'inizio dell'elemento che avvolge le informazioni per il tuo tipo. Cioè, poco prima del tag di inizio che indica l'inizio di un oggetto serializzato. Quando questo metodo ritorna, deve aver letto l'intero elemento dall'inizio alla fine, incluso tutto il suo contenuto. A differenza del metodo WriteXml, il framework non gestisce automaticamente l'elemento wrapper. L'implementazione deve farlo. La mancata osservanza di queste regole di posizionamento può far sì che il codice generi eccezioni di runtime impreviste o dati corrotti.

Concordo sul fatto che non è chiaro, ma si riduce a "è il tuo lavoro per Read()il tag dell'elemento finale del wrapper".


Che ne dici di scrivere e leggere gli elementi dell'evento? Sembra hacker scrivere manualmente l'elemento di partenza. Penso di aver visto qualcuno usare un XmlSerializer nel metodo write per scrivere ogni elemento figlio.
Greg,

@ Greg; entrambi gli usi vanno bene ... sì, puoi usare un XmlSerializer nidificato se ne hai bisogno, ma non è l'unica opzione.
Marc Gravell

3
Grazie per queste precisazioni, il codice di esempio all'interno di MSDN è piuttosto inutile e poco chiaro al riguardo. Mi sono bloccato molte volte e mi chiedevo quale fosse il comportamento asimmetrico di Read / WriteXml.
Jdehaan,

1
@MarcGravell So che questo è un vecchio thread. "Il framework scrive un elemento wrapper e posiziona il writer XML dopo il suo avvio." Questo è dove sto lottando. C'è un modo per forzare il framework a saltare questo passaggio della gestione automatica del wrapper? Ho una situazione in cui devo saltare questo passaggio: stackoverflow.com/questions/20885455/…
James

@James non al meglio delle mie conoscenze
Marc Gravell

34

Ho scritto un articolo sull'argomento con esempi poiché la documentazione MSDN è ormai poco chiara e gli esempi che puoi trovare sul web sono spesso implementati in modo errato.

Le insidie ​​riguardano la gestione di locali ed elementi vuoti accanto a quanto già menzionato da Marc Gravell.

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


Articolo eccellente! Lo farò sicuramente riferimento la prossima volta che cercherò di serializzare alcuni dati.
Greg,

Grazie! la quantità di feedback positivo premia la quantità di tempo investito nella sua scrittura. Apprezzo profondamente che ti piaccia! Non esitate a chiedere di criticare alcuni punti.
jdehaan,

Gli esempi sono molto più utili della citazione di MSDN.

Grazie per il codeproject, voterei anche questo, se potessi. Il materiale sugli attributi era completamente completo rispetto a MSDN. Ad esempio, la mia classe: IXMLSerializable si è interrotta quando il prefisso xsd.exe ha generato [Serializable (), XmlType (Namespace = "MonitorService")].
Giovanni,

8

Sì, il tutto è un po 'un campo minato, no? La risposta di Marc Gravell la copre praticamente, ma vorrei aggiungere che in un progetto a cui ho lavorato abbiamo trovato piuttosto imbarazzante dover scrivere manualmente l'elemento XML esterno. Ha inoltre prodotto nomi di elementi XML incoerenti per oggetti dello stesso tipo.

La nostra soluzione era quella di definire la nostra IXmlSerializableinterfaccia, derivata da quella di sistema, che aggiungeva un metodo chiamato WriteOuterXml(). Come puoi immaginare, questo metodo scriverà semplicemente l'elemento esterno, quindi chiamerà WriteXml(), quindi scriverà la fine dell'elemento. Naturalmente, il serializzatore XML di sistema non chiamerebbe questo metodo, quindi è stato utile solo quando abbiamo fatto la nostra serializzazione, quindi potrebbe essere utile o meno nel tuo caso. Allo stesso modo, abbiamo aggiunto un ReadContentXml()metodo, che non ha letto l'elemento esterno, ma solo il suo contenuto.


5
Con C # 3.0 puoi probabilmente farlo scrivendo un metodo di estensione, ma un'idea interessante.
Marc Gravell

2

Se hai già una rappresentazione XmlDocument della tua classe o preferisci il modo XmlDocument di lavorare con strutture XML, un modo rapido e sporco di implementare IXmlSerializable è semplicemente passare questo xmldoc alle varie funzioni.

ATTENZIONE: XmlDocument (e / o XDocument) è un ordine di grandezza più lento di xmlreader / writer, quindi se le prestazioni sono un requisito assoluto, questa soluzione non fa per te!

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

0

L'implementazione dell'interfaccia è coperta dalle altre risposte, ma volevo aggiungere i miei 2 centesimi per l'elemento root.

Ho imparato in passato a preferire mettere l'elemento radice come metadati. Questo ha alcuni vantaggi:

  • Se è presente un oggetto null, può ancora serializzare
  • Dal punto di vista della leggibilità del codice, ha senso

Di seguito è riportato un esempio di un dizionario serializzabile in cui l'elemento radice del dizionario è definito in questo modo:

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)
    {
    }

}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.