Question

Faisant suite à mon question précédente J'ai travaillé sur la sérialisation de mon modèle objet en XML.Mais j'ai maintenant rencontré un problème (quelle surprise !).

Le problème que j'ai est que j'ai une collection, qui est d'un type de classe de base abstraite, qui est peuplée de types dérivés concrets.

J'ai pensé que ce serait bien d'ajouter simplement les attributs XML à toutes les classes impliquées et que tout irait bien.Malheureusement, ce n'est pas le cas !

J'ai donc fait quelques recherches sur Google et je comprends maintenant pourquoi ça ne fonctionne pas.En cela le XmlSerializer est en fait en train de réfléchir astucieusement afin de sérialiser des objets vers/depuis XML, et comme il est basé sur le type abstrait, il ne peut pas comprendre à quoi il parle.Bien.

je suis tombé sur cette page sur CodeProject, ce qui semble pouvoir aider beaucoup (encore à lire/consommer complètement), mais j'ai pensé que j'aimerais également soulever ce problème dans la table StackOverflow, pour voir si vous avez des hacks/astuces intéressants afin de faites en sorte que cela soit opérationnel de la manière la plus rapide et la plus légère possible.

Une chose que je devrais également ajouter, c'est que je NE PAS je veux descendre le XmlInclude itinéraire.Il y a tout simplement trop de couplage avec lui, et cette partie du système est en plein développement, ce serait donc un véritable casse-tête de maintenance !

Était-ce utile?

La solution

Problème résolu!

OK, donc j'y suis finalement arrivé (certes avec un parcelle d'aide de ici!).

Alors résumons :

Objectifs:

  • Je ne voulais pas descendre XmlInclure route en raison du mal de tête d’entretien.
  • Une fois la solution trouvée, je souhaitais qu’elle soit rapidement implémentable dans d’autres applications.
  • Des collections de types abstraits peuvent être utilisées, ainsi que des propriétés abstraites individuelles.
  • Je ne voulais pas vraiment m'embêter à devoir faire des choses "spéciales" dans les cours concrets.

Problèmes identifiés/points à noter :

  • XmlSérialiseur fait une réflexion plutôt sympa, mais c'est très limité lorsqu'il s'agit de types abstraits (c'est-à-direcela ne fonctionnera qu'avec des instances du type abstrait lui-même, pas avec des sous-classes).
  • Les décorateurs d'attributs XML définissent la manière dont XmlSerializer traite les propriétés trouvées.Le type physique peut également être spécifié, mais cela crée un couplage serré entre la classe et le sérialiseur (pas bon).
  • Nous pouvons implémenter notre propre XmlSerializer en créant une classe qui implémente IXmlSérialisable .

La solution

J'ai créé une classe générique dans laquelle vous spécifiez le type générique comme type abstrait avec lequel vous allez travailler.Cela donne à la classe la possibilité de « traduire » entre le type abstrait et le type concret puisque nous pouvons coder en dur le casting (c'est-à-direnous pouvons obtenir plus d'informations que XmlSerializer).

J'ai ensuite mis en œuvre le IXmlSérialisable interface, c'est assez simple, mais lors de la sérialisation, nous devons nous assurer que nous écrivons le type de la classe concrète dans le XML, afin de pouvoir le restituer lors de la désérialisation.Il est également important de noter qu'il doit être pleinement qualifié car les assemblées dans lesquelles se trouvent les deux classes sont susceptibles de différer.Il y a bien sûr une petite vérification de type et des choses qui doivent se produire ici.

Puisque XmlSerializer ne peut pas effectuer de conversion, nous devons fournir le code pour le faire, donc l'opérateur implicite est alors surchargé (je ne savais même pas que vous pouviez faire cela !).

Le code de AbstractXmlSerializer est le suivant :

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

namespace Utility.Xml
{
    public class AbstractXmlSerializer<AbstractType> : IXmlSerializable
    {
        // Override the Implicit Conversions Since the XmlSerializer
        // Casts to/from the required types implicitly.
        public static implicit operator AbstractType(AbstractXmlSerializer<AbstractType> o)
        {
            return o.Data;
        }

        public static implicit operator AbstractXmlSerializer<AbstractType>(AbstractType o)
        {
            return o == null ? null : new AbstractXmlSerializer<AbstractType>(o);
        }

        private AbstractType _data;
        /// <summary>
        /// [Concrete] Data to be stored/is stored as XML.
        /// </summary>
        public AbstractType Data
        {
            get { return _data; }
            set { _data = value; }
        }

        /// <summary>
        /// **DO NOT USE** This is only added to enable XML Serialization.
        /// </summary>
        /// <remarks>DO NOT USE THIS CONSTRUCTOR</remarks>
        public AbstractXmlSerializer()
        {
            // Default Ctor (Required for Xml Serialization - DO NOT USE)
        }

        /// <summary>
        /// Initialises the Serializer to work with the given data.
        /// </summary>
        /// <param name="data">Concrete Object of the AbstractType Specified.</param>
        public AbstractXmlSerializer(AbstractType data)
        {
            _data = data;
        }

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null; // this is fine as schema is unknown.
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            // Cast the Data back from the Abstract Type.
            string typeAttrib = reader.GetAttribute("type");

            // Ensure the Type was Specified
            if (typeAttrib == null)
                throw new ArgumentNullException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because no 'type' attribute was specified in the XML.");

            Type type = Type.GetType(typeAttrib);

            // Check the Type is Found.
            if (type == null)
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the type specified in the XML was not found.");

            // Check the Type is a Subclass of the AbstractType.
            if (!type.IsSubclassOf(typeof(AbstractType)))
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the Type specified in the XML differs ('" + type.Name + "').");

            // Read the Data, Deserializing based on the (now known) concrete type.
            reader.ReadStartElement();
            this.Data = (AbstractType)new
                XmlSerializer(type).Deserialize(reader);
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            // Write the Type Name to the XML Element as an Attrib and Serialize
            Type type = _data.GetType();

            // BugFix: Assembly must be FQN since Types can/are external to current.
            writer.WriteAttributeString("type", type.AssemblyQualifiedName);
            new XmlSerializer(type).Serialize(writer, _data);
        }

        #endregion
    }
}

Alors, à partir de là, comment pouvons-nous dire à XmlSerializer de fonctionner avec notre sérialiseur plutôt qu'avec celui par défaut ?Nous devons passer notre type dans la propriété de type des attributs XML, par exemple :

[XmlRoot("ClassWithAbstractCollection")]
public class ClassWithAbstractCollection
{
    private List<AbstractType> _list;
    [XmlArray("ListItems")]
    [XmlArrayItem("ListItem", Type = typeof(AbstractXmlSerializer<AbstractType>))]
    public List<AbstractType> List
    {
        get { return _list; }
        set { _list = value; }
    }

    private AbstractType _prop;
    [XmlElement("MyProperty", Type=typeof(AbstractXmlSerializer<AbstractType>))]
    public AbstractType MyProperty
    {
        get { return _prop; }
        set { _prop = value; }
    }

    public ClassWithAbstractCollection()
    {
        _list = new List<AbstractType>();
    }
}

Ici vous pouvez voir que nous avons une collection et une seule propriété exposée, et tout ce que nous avons à faire est d'ajouter le taper paramètre nommé à la déclaration XML, facile !:D

NOTE:Si vous utilisez ce code, j'apprécierais vraiment un message.Cela contribuera également à attirer plus de personnes vers la communauté :)

Maintenant, mais je ne sais pas quoi faire des réponses ici, car elles avaient toutes leurs avantages et leurs inconvénients.Je mettrai à jour ceux qui me semblent utiles (n'en déplaise à ceux qui ne l'étaient pas) et je clôturerai cela une fois que j'aurai le représentant :)

Problème intéressant et très amusant à résoudre !:)

Autres conseils

Une chose à considérer est le fait que dans le constructeur XmlSerialiser, vous pouvez transmettre un tableau de types que le sérialiseur pourrait avoir des difficultés à résoudre.J'ai dû l'utiliser à plusieurs reprises lorsqu'une collection ou un ensemble complexe de structures de données devait être sérialisé et que ces types vivaient dans différents assemblys, etc.

Constructeur XmlSerialiser avec paramètre extraTypes

MODIFIER:J'ajouterais que cette approche présente l'avantage par rapport aux attributs XmlInclude, etc., de pouvoir trouver un moyen de découvrir et de compiler une liste de vos types concrets possibles au moment de l'exécution et de les insérer.

Sérieusement, un cadre extensible de POCO ne sera jamais sérialisé en XML de manière fiable.Je dis cela parce que je peux garantir que quelqu'un viendra, prolongera votre cours et le bâclera.

Vous devriez envisager d'utiliser XAML pour sérialiser vos graphiques d'objets.Il est conçu pour cela, contrairement à la sérialisation XML.

Le sérialiseur et désérialiseur Xaml gère sans problème les génériques, ainsi que les collections de classes de base et d'interfaces (à condition que les collections elles-mêmes implémentent IList ou IDictionary).Il existe quelques mises en garde, telles que le marquage des propriétés de votre collection en lecture seule avec le DesignerSerializationAttribute, mais retravailler votre code pour gérer ces cas extrêmes n'est pas si difficile.

Juste un petit point à ce sujet, je n'ai pas oublié !

Je fais juste quelques recherches supplémentaires, on dirait que je suis sur un gagnant, j'ai juste besoin de trier le code.

Pour l'instant, j'ai ce qui suit :

  • Le XmlSéraliseur est fondamentalement une classe qui fait une réflexion intéressante sur les classes qu'elle sérialise.Il détermine les propriétés qui sont sérialisées en fonction du Taper.
  • La raison pour laquelle le problème se produit est qu'une incompatibilité de type se produit, il attend le Type de base mais reçoit en fait le Type dérivé ..Bien que vous puissiez penser qu'il le traiterait de manière polymorphe, ce n'est pas le cas car cela impliquerait toute une charge supplémentaire de réflexion et de vérification de type, ce pour quoi il n'est pas conçu.

Ce comportement semble pouvoir être remplacé (code en attente) en créant une classe proxy pour servir d'intermédiaire pour le sérialiseur.Cela déterminera essentiellement le type de la classe dérivée, puis la sérialisera normalement.Cette classe proxy transmettra ensuite ce XML à la ligne de sauvegarde vers le sérialiseur principal.

Surveillez cet endroit!^_^

C'est certainement une solution à votre problème, mais il existe un autre problème qui mine quelque peu votre intention d'utiliser le format XML "portable".Une mauvaise chose se produit lorsque vous décidez de changer de classe dans la prochaine version de votre programme et que vous devez prendre en charge les deux formats de sérialisation : le nouveau et l'ancien (car vos clients utilisent toujours leurs anciens fichiers/bases de données, ou ils se connectent à votre serveur utilisant l'ancienne version de votre produit).Mais vous ne pouvez plus utiliser ce sérialiseur, car vous avez utilisé

type.AssemblyQualifiedName

qui ressemble

TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089

qui contient les attributs et la version de votre assembly...

Maintenant, si vous essayez de changer votre version d'assembly, ou si vous décidez de la signer, cette désérialisation ne fonctionnera pas...

J'ai fait des choses similaires à celle-ci.Ce que je fais normalement, c'est m'assurer que tous les attributs de sérialisation XML se trouvent sur la classe concrète et que les propriétés de cette classe appellent les classes de base (si nécessaire) pour récupérer les informations qui seront désérialisées lorsque le sérialiseur appelle. ces propriétés.C'est un peu plus de travail de codage, mais cela fonctionne bien mieux que d'essayer de forcer le sérialiseur à faire ce qu'il faut.

Mieux encore, en utilisant la notation :

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top