Forma correcta de implementar IXmlSerializable?

153

Una vez que un programador decide implementar IXmlSerializable, ¿cuáles son las reglas y las mejores prácticas para implementarlo? Escuché que GetSchema()debería regresar nully ReadXmldebería pasar al siguiente elemento antes de regresar. ¿Es esto cierto? ¿Y qué pasa si WriteXmldebería escribir un elemento raíz para el objeto o se supone que la raíz ya está escrita? ¿Cómo se deben tratar y escribir los objetos secundarios?

Aquí hay una muestra de lo que tengo ahora. Lo actualizaré a medida que obtenga buenas respuestas.

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 de muestra correspondiente

<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
fuente
3
¿Podría agregar una muestra xml a esta pregunta? Sería más fácil leer junto con el código. ¡Gracias!
Rory
¿Qué pasa con el caso en el que hay un comentario XML, etc. después del último evento en su xml? es decir, ¿debería terminar el método ReadXml () con algo que verifique que haya leído hasta el elemento final? Actualmente esto supone que el último Read () hace eso, pero puede que no siempre sea así.
Rory
77
@Rory - Muestra agregada. ¿Mejor tarde que nunca?
Greg
@Greg Buena información. ¿No le gustaría que ReadXml y WriteXml utilicen la Cultura Invariante? Creo que puede tener problemas si el usuario se mudó a otro país y cambió su Configuración de región e idioma. En ese caso, el código podría no deserializarse correctamente. He leído que es una buena práctica usar siempre la Cultura Invariante cuando se realiza la serialización
inalámbrica pública

Respuestas:

100

Sí, GetSchema () debería devolver nulo .

Método IXmlSerializable.GetSchema Este método está reservado y no debe utilizarse. Al implementar la interfaz IXmlSerializable, debe devolver una referencia nula (Nothing en Visual Basic) de este método y, en su lugar, si se requiere un esquema personalizado, aplique XmlSchemaProviderAttribute a la clase.

Tanto para lectura como para escritura, el elemento objeto ya se ha escrito, por lo que no necesita agregar un elemento externo en escritura. Por ejemplo, puede comenzar a leer / escribir atributos en los dos.

Para escribir :

La implementación de WriteXml que proporcione debe escribir la representación XML del objeto. El marco escribe un elemento contenedor y posiciona al escritor XML después de su inicio. Su implementación puede escribir su contenido, incluidos los elementos secundarios. El marco luego cierra el elemento contenedor.

Y para leer :

El método ReadXml debe reconstituir su objeto utilizando la información escrita por el método WriteXml.

Cuando se llama a este método, el lector se coloca al comienzo del elemento que envuelve la información para su tipo. Es decir, justo antes de la etiqueta de inicio que indica el comienzo de un objeto serializado. Cuando este método regresa, debe haber leído todo el elemento de principio a fin, incluido todo su contenido. A diferencia del método WriteXml, el marco no maneja el elemento contenedor automáticamente. Su implementación debe hacerlo. El incumplimiento de estas reglas de posicionamiento puede hacer que el código genere excepciones inesperadas de tiempo de ejecución o datos corruptos.

Estoy de acuerdo en que no está claro, pero se reduce a "es tu trabajo Read()la etiqueta del elemento final del contenedor".

Marc Gravell
fuente
¿Qué hay de escribir y leer los elementos del evento? Se siente hackear escribir manualmente el elemento inicial. Creo que he visto a alguien usar un XmlSerializer en el método de escritura para escribir cada elemento secundario.
Greg
@Greg; cualquier uso está bien ... sí, puede usar un XmlSerializer anidado si lo necesita, pero no es la única opción.
Marc Gravell
3
Gracias por estas precisiones, el código de muestra dentro de MSDN es bastante inútil y poco claro al respecto. Me atasqué muchas veces y me preguntaba sobre el comportamiento asimétrico de Read / WriteXml.
jdehaan
1
@MarcGravell Sé que este es un hilo viejo. "El marco escribe un elemento contenedor y posiciona al escritor XML después de su inicio". Aquí es donde estoy luchando. ¿Hay alguna manera de forzar que el marco omita este paso de manejar automáticamente el contenedor? Tengo una situación en la que necesito omitir este paso: stackoverflow.com/questions/20885455/…
James
@James, que yo sepa
Marc Gravell
34

Escribí un artículo sobre el tema con ejemplos, ya que la documentación de MSDN no está clara y los ejemplos que puede encontrar en la web se implementan incorrectamente la mayoría de las veces.

Las trampas son el manejo de locales y elementos vacíos junto a lo que Marc Gravell ya mencionó.

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

jdehaan
fuente
Excelente articulo! Definitivamente lo mencionaré la próxima vez que busque serializar algunos datos.
Greg
¡Gracias! la cantidad de comentarios positivos recompensa la cantidad de tiempo invertido en escribirlo. ¡Aprecio profundamente que te guste! No dudes en pedirnos criticar algunos puntos.
jdehaan
Los ejemplos son mucho más útiles que citar MSDN.
Gracias por el proyecto de código, también lo votaría si pudiera. El contenido de los atributos fue completamente completo en comparación con MSDN. Por ejemplo, mi: clase IXMLSerializable se rompió cuando el prefijo xsd.exe generado [Serializable (), XmlType (Namespace = "MonitorService")].
John
8

Sí, todo es un campo minado, ¿no? La respuesta de Marc Gravell lo cubre más o menos, pero me gustaría agregar que en un proyecto en el que trabajé nos pareció bastante incómodo tener que escribir manualmente el elemento XML externo. También resultó en nombres de elementos XML inconsistentes para objetos del mismo tipo.

Nuestra solución fue definir nuestra propia IXmlSerializableinterfaz, derivada de la del sistema, que agregó un método llamado WriteOuterXml(). Como puede adivinar, este método simplemente escribiría el elemento externo, luego llamaría WriteXml()y luego escribiría el final del elemento. Por supuesto, el serializador XML del sistema no llamaría a este método, por lo que solo fue útil cuando hicimos nuestra propia serialización, por lo que puede o no ser útil en su caso. Del mismo modo, agregamos un ReadContentXml()método, que no leía el elemento externo, solo su contenido.

EMP
fuente
55
Con C # 3.0, probablemente pueda hacer esto escribiendo un método de extensión, pero una idea interesante.
Marc Gravell
2

Si ya tiene una representación XmlDocument de su clase o prefiere la forma XmlDocument de trabajar con estructuras XML, una forma rápida y sucia de implementar IXmlSerializable es simplemente pasar este xmldoc a las diversas funciones.

ADVERTENCIA: XmlDocument (y / o XDocument) es un orden de magnitud más lento que xmlreader / writer, por lo que si el rendimiento es un requisito absoluto, ¡esta solución no es para usted!

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

La implementación de la interfaz está cubierta por las otras respuestas, pero quería agregar mis 2 centavos para el elemento raíz.

En el pasado aprendí a preferir colocar el elemento raíz como metadatos. Esto tiene algunos beneficios:

  • Si hay un objeto nulo, aún puede serializar
  • Desde el punto de vista de la legibilidad del código, tiene sentido

A continuación se muestra un ejemplo de un diccionario serializable donde el elemento raíz del diccionario se define de esa manera:

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

}
VotoCafé
fuente