Siguiendo con mi pregunta anterior , he estado trabajando para que mi modelo de objetos se serialice en XML. Pero ahora me he encontrado con un problema (¡quelle sorpresa!).
El problema que tengo es que tengo una colección, que es de un tipo de clase base abstracta, que está poblada por los tipos derivados concretos.
Pensé que estaría bien simplemente agregar los atributos XML a todas las clases involucradas y todo sería perfecto. Lamentablemente, ¡ese no es el caso!
Así que he investigado un poco en Google y ahora entiendo por qué no funciona. En ese el XmlSerializer
es, de hecho, haciendo una reflexión inteligente con el fin de serializar objetos a / desde XML, y desde su base en el tipo abstracto, no puede averiguar qué diablos está hablando . Multa.
Me encontré con esta página en CodeProject, que parece que puede ayudar mucho (aún para leer / consumir por completo), pero pensé que también me gustaría llevar este problema a la tabla StackOverflow, para ver si tiene algo ordenado hacks / trucos para que esto funcione de la manera más rápida / ligera posible.
Una cosa que también debo agregar es que NO quiero seguir la XmlInclude
ruta. Simplemente hay demasiado acoplamiento con él, y esta área del sistema está en un gran desarrollo, por lo que sería un verdadero dolor de cabeza de mantenimiento.
fuente
Respuestas:
¡Problema resuelto!
Bien, finalmente llegué allí (¡es cierto que con mucha ayuda de aquí !).
Así que resuma:
Metas:
Problemas identificados / puntos a tener en cuenta:
La solución
Creé una clase genérica, en la que especificas el tipo genérico como el tipo abstracto con el que trabajarás. Esto le da a la clase la capacidad de "traducir" entre el tipo abstracto y el tipo concreto, ya que podemos codificar la conversión (es decir, podemos obtener más información de la que puede obtener el XmlSerializer).
Luego implementé la interfaz IXmlSerializable , esto es bastante sencillo, pero al serializar debemos asegurarnos de escribir el tipo de la clase concreta en el XML, para que podamos devolverlo al deserializar. También es importante tener en cuenta que debe estar completamente calificado ya que es probable que los ensamblados en los que se encuentran las dos clases difieran. Por supuesto, hay una pequeña verificación de tipos y cosas que deben suceder aquí.
Dado que XmlSerializer no puede emitir, necesitamos proporcionar el código para hacer eso, por lo que el operador implícito se sobrecarga (¡ni siquiera sabía que podía hacer esto!).
El código para AbstractXmlSerializer es este:
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 } }
Entonces, a partir de ahí, ¿cómo le decimos al XmlSerializer que funcione con nuestro serializador en lugar del predeterminado? Debemos pasar nuestro tipo dentro de la propiedad de tipo de atributos Xml, por ejemplo:
[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>(); } }
Aquí puede ver, tenemos una colección y una sola propiedad expuesta, y todo lo que tenemos que hacer es agregar el parámetro con nombre de tipo a la declaración Xml, ¡fácil! :RE
NOTA: Si usa este código, le agradecería mucho un reconocimiento. También ayudará a atraer a más personas a la comunidad :)
Ahora, pero no estoy seguro de qué hacer con las respuestas aquí, ya que todos tenían sus pros y sus contras. Mejoraré los que considero útiles (sin ofender a los que no lo sean) y cerraré esto una vez que tenga la reputación :)
¡Problema interesante y divertido de resolver! :)
fuente
private
oprotected
hacer cumplir que no esté disponible para otras clases.Una cosa a tener en cuenta es el hecho de que en el constructor XmlSerialiser puede pasar una serie de tipos que el serializador podría tener dificultades para resolver. Tuve que usar eso varias veces cuando una colección o un conjunto complejo de estructuras de datos necesitaban ser serializados y esos tipos vivían en diferentes ensamblajes, etc.
Constructor XmlSerialiser con extraTypes param
EDITAR: Yo agregaría que este enfoque tiene el beneficio sobre los atributos XmlInclude, etc. de que puede encontrar una forma de descubrir y compilar una lista de sus posibles tipos concretos en tiempo de ejecución y rellenarlos.
fuente
En serio, un marco extensible de POCO nunca se serializará en XML de manera confiable. Digo esto porque puedo garantizar que alguien vendrá, extenderá su clase y lo arruinará.
Debería considerar el uso de XAML para serializar sus gráficos de objetos. Está diseñado para hacer esto, mientras que la serialización XML no lo es.
El serializador y deserializador Xaml maneja genéricos sin problemas, colecciones de clases base e interfaces también (siempre que las propias colecciones implementen
IList
oIDictionary
). Hay algunas advertencias, como marcar las propiedades de la colección de solo lectura con elDesignerSerializationAttribute
, pero reelaborar su código para manejar estos casos de esquina no es tan difícil.fuente
Solo una actualización rápida sobre esto, ¡no lo he olvidado!
Solo estoy investigando un poco más, parece que estoy en un ganador, solo necesito ordenar el código.
Hasta ahora, tengo lo siguiente:
Este comportamiento parece poder anularse (código pendiente) creando una clase de proxy para actuar como intermediario para el serializador. Esto básicamente determinará el tipo de clase derivada y luego lo serializará como de costumbre. Esta clase de proxy luego alimentará ese XML de respaldo de la línea al serializador principal.
¡Mira este espacio! ^ _ ^
fuente
Ciertamente es una solución a su problema, pero hay otro problema, que de alguna manera socava su intención de utilizar el formato XML "portátil". Sucede algo malo cuando decide cambiar las clases en la próxima versión de su programa y necesita admitir ambos formatos de serialización, el nuevo y el anterior (porque sus clientes todavía usan sus archivos / bases de datos antiguos, o se conectan a su servidor usando la versión anterior de su producto). Pero ya no puede usar este serializador, porque usó
type.AssemblyQualifiedName
que parece
TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089
que contiene sus atributos de ensamblaje y versión ...
Ahora bien, si intenta cambiar su versión de ensamblado, o decide firmarlo, esta deserialización no va a funcionar ...
fuente
He hecho cosas similares a esto. Lo que normalmente hago es asegurarme de que todos los atributos de serialización XML estén en la clase concreta, y solo hacer que las propiedades de esa clase llamen a las clases base (donde sea necesario) para recuperar información que se eliminará / serializará cuando el serializador llame a esas propiedades. Es un poco más de trabajo de codificación, pero funciona mucho mejor que intentar forzar al serializador a hacer lo correcto.
fuente
Aún mejor, usando la notación:
[XmlRoot] public class MyClass { public abstract class MyAbstract {} public class MyInherited : MyAbstract {} [XmlArray(), XmlArrayItem(typeof(MyInherited))] public MyAbstract[] Items {get; set; } }
fuente