Suite à ma question précédente, j'ai travaillé sur la sérialisation de mon modèle objet vers 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 abstrait, qui est peuplée par les 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 serait pêche. Malheureusement, ce n'est pas le cas!
J'ai donc fait quelques recherches sur Google et je comprends maintenant pourquoi cela ne fonctionne pas. En fait, le XmlSerializer
fait en fait une réflexion intelligente 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, qui semble que cela pourrait bien aider (encore à lire / consommer pleinement), mais je pensais que je voudrais également apporter ce problème à la table StackOverflow, pour voir si vous en avez hacks / astuces pour que cela soit opérationnel de la manière la plus rapide / la plus légère possible.
Une chose que je devrais également ajouter est que je ne veux PAS emprunter cette XmlInclude
voie. Il y a tout simplement trop de couplage avec celui-ci, et cette zone du système est en plein développement, ce serait donc un véritable casse-tête de maintenance!
la source
Réponses:
Problème résolu!
OK, alors j'y suis enfin arrivé (certes avec beaucoup d'aide d' ici !).
Alors résumez:
Buts:
Problèmes identifiés / points à noter:
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 travaillerez. 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-à-dire que nous pouvons obtenir plus d'informations que le XmlSerializer).
J'ai ensuite implémenté l' interface IXmlSerializable , 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 renvoyer lors de la désérialisation. Il est également important de noter qu'il doit être entièrement qualifié car les assemblys dans lesquels 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.
Étant donné que XmlSerializer ne peut pas effectuer de conversion, nous devons fournir le code pour le faire, de sorte que l'opérateur implicite est alors surchargé (je n'ai même jamais su que vous pouviez le faire!).
Le code pour 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 dire au XmlSerializer de fonctionner avec notre sérialiseur plutôt qu'avec le sérialiseur 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, nous avons une collection et une seule propriété exposée, et tout ce que nous avons à faire est d'ajouter le paramètre de type nommé à la déclaration Xml, facile! :RÉ
REMARQUE: Si vous utilisez ce code, j'apprécierais vraiment un cri. Cela aidera également à attirer plus de personnes dans la communauté :)
Maintenant, mais je ne sais pas quoi faire avec les réponses ici, car ils avaient tous leurs avantages et leurs inconvénients. Je vais améliorer ceux que je trouve utiles (sans offenser ceux qui ne le sont pas) et fermer cela une fois que j'ai le représentant :)
Problème intéressant et très amusant à résoudre! :)
la source
private
ouprotected
pour imposer qu'il ne soit pas disponible pour d'autres classes.Une chose à considérer est le fait que dans le constructeur XmlSerialiser, vous pouvez passer un tableau de types que le sérialiseur pourrait avoir des difficultés à résoudre. J'ai dû l'utiliser plusieurs fois lorsqu'une collection ou un ensemble complexe de structures de données devait être sérialisé et que ces types vivaient dans différents assemblages, etc.
Constructeur XmlSerialiser avec paramètre extraTypes
EDIT: J'ajouterais que cette approche a l'avantage sur les attributs XmlInclude, etc. que vous pouvez 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 remplir.
la source
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 les génériques sans problème, ainsi que les collections de classes de base et d'interfaces (tant que les collections elles-mêmes implémentent
IList
ouIDictionary
). Il y a quelques mises en garde, telles que le marquage de vos propriétés de collection en lecture seule avec leDesignerSerializationAttribute
, mais retravailler votre code pour gérer ces cas d'angle n'est pas si difficile.la source
Juste une petite mise à jour à ce sujet, je n'ai pas oublié!
Je fais juste quelques recherches supplémentaires, on dirait que je suis sur un gagnant, juste besoin de faire trier le code.
Jusqu'à présent, j'ai ce qui suit:
Ce comportement semble pouvoir être remplacé (code en attente) en créant une classe proxy qui servira 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 alimentera ensuite cette sauvegarde XML de la ligne vers le sérialiseur principal.
Surveillez cet endroit! ^ _ ^
la source
C'est certainement une solution à votre problème, mais il y a 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érialisateur, car vous avez utilisé
type.AssemblyQualifiedName
qui ressemble à
TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089
qui contient vos attributs d'assemblage et votre version ...
Maintenant, si vous essayez de changer la version de votre assembly, ou si vous décidez de la signer, cette désérialisation ne fonctionnera pas ...
la source
J'ai fait des choses similaires. Ce que je fais normalement est de m'assurer que tous les attributs de sérialisation XML sont sur la classe concrète, et que les propriétés de cette classe appellent simplement 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 beaucoup mieux que d'essayer de forcer le sérialiseur à faire ce qu'il faut.
la source
Encore mieux, 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; } }
la source