Problème Résolu!
D'accord, j'y suis enfin arrivé (admettons-le avec beaucoup d'aide de ici!).
Donc, pour résumer :
Objectifs :
- Je ne voulais pas suivre la voie de XmlInclude en raison du casse-tête de maintenance.
- Une fois une solution trouvée, je voulais qu'elle soit rapide à mettre en œuvre 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 me donner la peine de faire des choses "spéciales" dans les classes concrètes.
Problèmes Identifiés/Points à Noter :
- XmlSerializer fait une réflexion vraiment cool, mais il est très limité en ce qui concerne les types abstraits (c'est-à-dire qu'il ne fonctionnera qu'avec les instances du type abstrait lui-même, pas les sous-classes).
- Les décorateurs d'attributs Xml définissent comment XmlSerializer traite les propriétés qu'il trouve. Le type physique peut également être spécifié, mais cela crée un couplage fort entre la classe et le sérialiseur (pas bien).
- Nous pouvons implémenter notre propre XmlSerializer en créant une classe qui implémente IXmlSerializable.
La Solution
J'ai créé une classe générique, dans laquelle vous spécifiez le type générique comme le type abstrait avec lequel vous travaillerez. Cela donne à la classe la capacité de "traduire" entre le type abstrait et le type concret car nous pouvons coder en dur le casting (c'est-à-dire que nous pouvons obtenir plus d'informations que le XmlSerializer ne peut).
J'ai ensuite implémenté l'interface IXmlSerializable, cela est assez simple, mais lors de la sérialisation, nous devons nous assurer d'écrire le type de la classe concrète dans le XML, afin que nous puissions le caster lorsque nous désérialisons. Il est également important de noter qu'il doit être entièrement qualifié car les assemblies dans lesquelles se trouvent les deux classes sont susceptibles de différer. Bien sûr, il y a un peu de vérification de type et des choses qui doivent se produire ici.
Puisque XmlSerializer ne peut pas caster, nous devons fournir le code pour le faire, donc l'opérateur implicite est ensuite surchargé (je ne savais même pas que vous pouviez faire ça!).
Le code pour l'AbstractXmlSerializer est le suivant :
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
namespace Utility.Xml
{
public class AbstractXmlSerializer : IXmlSerializable
{
// Remplace les conversions implicites, car le XmlSerializer
// casting vers/depuis les types requis implicitement.
public static implicit operator AbstractType(AbstractXmlSerializer o)
{
return o.Data;
}
public static implicit operator AbstractXmlSerializer(AbstractType o)
{
return o == null ? null : new AbstractXmlSerializer(o);
}
private AbstractType _data;
///
/// Données [concrètes] à stocker/stockées en tant que XML.
///
public AbstractType Data
{
get { return _data; }
set { _data = value; }
}
///
/// **NE PAS UTILISER** Ceci est uniquement ajouté pour permettre la serialisation XML.
///
/// NE PAS UTILISER CE CONSTRUCTEUR
public AbstractXmlSerializer()
{
// Constructeur par défaut (Requis pour la serialisation Xml - NE PAS UTILISER)
}
///
/// Initialise le sérialiseur pour fonctionner avec les données fournies.
///
/// Objet concret du type abstrait spécifié.
public AbstractXmlSerializer(AbstractType data)
{
_data = data;
}
#region Membres IXmlSerializable
public System.Xml.Schema.XmlSchema GetSchema()
{
return null; // c'est bon car le schéma est inconnu.
}
public void ReadXml(System.Xml.XmlReader reader)
{
// Caster les données en arrière du type abstrait.
string typeAttrib = reader.GetAttribute("type");
// Assurer que le type a été spécifié
if (typeAttrib == null)
throw new ArgumentNullException("Impossible de lire les données Xml pour le type abstrait '" + typeof(AbstractType).Name +
"' car aucun attribut 'type' n'a été spécifié dans le XML.");
Type type = Type.GetType(typeAttrib);
// Vérifier si le type est trouvé.
if (type == null)
throw new InvalidCastException("Impossible de lire les données Xml pour le type abstrait '" + typeof(AbstractType).Name +
"' car le type spécifié dans le XML n'a pas été trouvé.");
// Vérifier que le type est une sous-classe du type abstrait.
if (!type.IsSubclassOf(typeof(AbstractType)))
throw new InvalidCastException("Impossible de lire les données Xml pour le type abstrait '" + typeof(AbstractType).Name +
"' car le type spécifié dans le XML diffère ('" + type.Name + "').");
// Lire les données, désérialiser en fonction du type concret (maintenant connu).
reader.ReadStartElement();
this.Data = (AbstractType)new
XmlSerializer(type).Deserialize(reader);
reader.ReadEndElement();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
// Écrire le nom du type dans l'élément XML en tant qu'attribut et serialiser
Type type = _data.GetType();
// Correctif de bug : L'assembly doit être un FQN puisque les types peuvent/étaient externes à l'actuel.
writer.WriteAttributeString("type", type.AssemblyQualifiedName);
new XmlSerializer(type).Serialize(writer, _data);
}
#endregion
}
}
Alors, à partir de là, comment dire au XmlSerializer de travailler avec notre sérialiseur plutôt qu'avec le défaut ? Nous devons passer notre type dans la propriété nommée type des attributs Xml, par exemple :
[XmlRoot("ClasseAvecCollectionAbstraite")]
public class ClasseAvecCollectionAbstraite
{
private List _list;
[XmlArray("ElementsDeListe")]
[XmlArrayItem("ElementDeListe", Type = typeof(AbstractXmlSerializer))]
public List Liste
{
get { return _list; }
set { _list = value; }
}
private AbstractType _prop;
[XmlElement("MaPropriété", Type=typeof(AbstractXmlSerializer))]
public AbstractType MaPropriété
{
get { return _prop; }
set { _prop = value; }
}
public ClasseAvecCollectionAbstraite()
{
_list = new List();
}
}
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 nommé type à la déclaration Xml, facile ! :D
REMARQUE: Si vous utilisez ce code, j'apprécierais vraiment un petit coup de pouce. Cela aidera également à attirer plus de personnes vers la communauté :)
Maintenant, mais incertain de ce qu'il faut faire avec les réponses ici car elles avaient toutes leurs avantages et inconvénients. Je vais upvoter ceux que je trouve utiles (sans offenser ceux qui ne le sont pas) et fermer cela une fois que j'aurai la réputation :)
Problème intéressant et amusant à résoudre! :)