Comment implémenter IXmlSerializable?

153

Une fois qu'un programmeur décide de l'implémenter IXmlSerializable, quelles sont les règles et les meilleures pratiques pour l'implémenter? J'ai entendu dire que cela GetSchema()devrait revenir nullet ReadXmlpasser à l'élément suivant avant de revenir. Est-ce vrai? Et qu'en WriteXmlest-il - doit-il écrire un élément racine pour l'objet ou est-il supposé que la racine est déjà écrite? Comment les objets enfants doivent-ils être traités et écrits?

Voici un échantillon de ce que j'ai maintenant. Je vais le mettre à jour au fur et à mesure que j'obtiens de bonnes réponses.

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

Échantillon XML correspondant

<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
la source
3
Pourriez-vous ajouter un exemple XML à cette question? Cela simplifierait la lecture avec le code. Merci!
Rory le
Qu'en est-il du traitement du cas où il y a un commentaire XML, etc. après le dernier événement dans votre xml. c'est-à-dire devez-vous terminer la méthode ReadXml () avec quelque chose qui vérifie que vous avez lu jusqu'à l'élément final? Actuellement, cela suppose que le dernier Read () le fait, mais ce n'est pas toujours le cas.
Rory
7
@Rory - Échantillon ajouté. Mieux vaut tard que jamais?
Greg
@Greg Bonne info. Ne voudriez-vous pas également que ReadXml et WriteXml utilisent la culture invariante? Je pense que vous pouvez rencontrer des problèmes si l'utilisateur a déménagé dans un autre pays et a changé ses paramètres de région et de langue. Dans ce cas, le code peut ne pas désérialiser correctement. J'ai lu qu'il est recommandé de toujours utiliser la culture invariante lors de la sérialisation
sans fil public

Réponses:

100

Oui, GetSchema () doit retourner null .

IXmlSerializable.GetSchema, méthode Cette méthode est réservée et ne doit pas être utilisée. Lors de l'implémentation de l'interface IXmlSerializable, vous devez renvoyer une référence null (Nothing en Visual Basic) à partir de cette méthode et, à la place, si la spécification d'un schéma personnalisé est requise, appliquez XmlSchemaProviderAttribute à la classe.

Pour la lecture et l'écriture, l'élément objet a déjà été écrit, vous n'avez donc pas besoin d'ajouter un élément externe en écriture. Par exemple, vous pouvez simplement commencer à lire / écrire des attributs dans les deux.

Pour écrire :

L'implémentation WriteXml que vous fournissez doit écrire la représentation XML de l'objet. Le framework écrit un élément wrapper et positionne l'enregistreur XML après son démarrage. Votre implémentation peut écrire son contenu, y compris les éléments enfants. Le framework ferme ensuite l'élément wrapper.

Et pour lire :

La méthode ReadXml doit reconstituer votre objet à l'aide des informations écrites par la méthode WriteXml.

Lorsque cette méthode est appelée, le lecteur est positionné au début de l'élément qui encapsule les informations pour votre type. Autrement dit, juste avant la balise de début qui indique le début d'un objet sérialisé. Lorsque cette méthode revient, elle doit avoir lu tout l'élément du début à la fin, y compris tout son contenu. Contrairement à la méthode WriteXml, le framework ne gère pas automatiquement l'élément wrapper. Votre implémentation doit le faire. Le non-respect de ces règles de positionnement peut entraîner la génération d'exceptions d'exécution inattendues ou des données corrompues par le code.

Je conviens que ce n'est pas clair, mais cela se résume à "c'est votre travail à Read()la balise d'élément de fin de l'emballage".

Marc Gravell
la source
Qu'en est-il de l'écriture et de la lecture des éléments de l'événement? C'est un peu bizarre d'écrire manuellement l'élément de départ. Je pense avoir vu quelqu'un utiliser un XmlSerializer dans la méthode d'écriture pour écrire chaque élément enfant.
Greg
@Greg; soit l'utilisation est bonne ... oui, vous pouvez utiliser un XmlSerializer imbriqué si vous en avez besoin, mais ce n'est pas la seule option.
Marc Gravell
3
Merci pour ces précisions, l'exemple de code à l'intérieur de MSDN est assez inutile et peu clair à ce sujet. Je suis resté coincé plusieurs fois et je m'interrogeais sur le comportement asymétrique de Read / WriteXml.
jdehaan
1
@MarcGravell Je sais que c'est un vieux fil. "Le framework écrit un élément wrapper et positionne le rédacteur XML après son démarrage." C'est là que je lutte. Existe-t-il un moyen de forcer le framework à sauter cette étape de gestion automatique du wrapper? J'ai une situation où je dois sauter cette étape: stackoverflow.com/questions/20885455/…
James
@James pas au meilleur de ma connaissance
Marc Gravell
34

J'ai écrit un article sur le sujet avec des exemples car la documentation MSDN est maintenant assez floue et les exemples que vous pouvez trouver sur le Web sont la plupart du temps mal implémentés.

Les pièges sont la gestion des locales et des éléments vides à côté de ce que Marc Gravell a déjà mentionné.

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

jdehaan
la source
Excellent article! Je vais certainement y faire référence la prochaine fois que je cherche à sérialiser certaines données.
Greg
Merci! la quantité de rétroaction positive récompense le temps investi dans sa rédaction. J'apprécie profondément que vous l'aimiez! N'hésitez pas à demander à critiquer certains points.
jdehaan
Les exemples sont bien plus utiles que de citer MSDN.
Merci pour le projet de code, je voterais cela aussi, si je le pouvais. Le truc sur les attributs était tout à fait complet par rapport à MSDN. Par exemple, ma classe: IXMLSerializable s'est cassée lorsqu'elle était préfixée par le xsd.exe généré [Serializable (), XmlType (Namespace = "MonitorService")].
John
8

Oui, le tout est un peu un champ de mines, n'est-ce pas? La réponse de Marc Gravell le couvre à peu près, mais j'aimerais ajouter que dans un projet sur lequel j'ai travaillé, nous avons trouvé assez gênant d'avoir à écrire manuellement l'élément XML externe. Cela entraînait également des noms d'éléments XML incohérents pour les objets du même type.

Notre solution a été de définir notre propre IXmlSerializableinterface, dérivée de celle du système, qui a ajouté une méthode appelée WriteOuterXml(). Comme vous pouvez le deviner, cette méthode écrirait simplement l'élément extérieur, puis appellerait WriteXml(), puis écrirait la fin de l'élément. Bien sûr, le sérialiseur XML système n'appellerait pas cette méthode, donc cela n'était utile que lorsque nous faisions notre propre sérialisation, donc cela peut ou non être utile dans votre cas. De même, nous avons ajouté une ReadContentXml()méthode, qui n'a pas lu l'élément externe, uniquement son contenu.

EMP
la source
5
Avec C # 3.0, vous pouvez probablement le faire en écrivant une méthode d'extension à la place, mais une idée intéressante.
Marc Gravell
2

Si vous avez déjà une représentation XmlDocument de votre classe ou préférez la manière XmlDocument de travailler avec des structures XML, une manière rapide et sale d'implémenter IXmlSerializable consiste simplement à transmettre ce xmldoc aux différentes fonctions.

AVERTISSEMENT: XmlDocument (et / ou XDocument) est un ordre de grandeur plus lent que xmlreader / writer, donc si les performances sont une exigence absolue, cette solution n'est pas pour vous!

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
la source
0

L'implémentation de l'interface est couverte par les autres réponses, mais je voulais ajouter mes 2 cents pour l'élément racine.

J'ai appris dans le passé à préférer mettre l'élément racine en tant que métadonnées. Cela présente quelques avantages:

  • S'il y a un objet nul, il peut toujours sérialiser
  • Du point de vue de la lisibilité du code, cela a du sens

Voici un exemple de dictionnaire sérialisable où l'élément racine du dictionnaire est défini de cette manière:

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

}
VoteCafé
la source