DNM+ Online
dotnetmania 2.0
Serialización XML El serializador que lo serialice... debe saber XML
En el artículo anterior vimos cómo “persistir” los objetos usando las clases que utilizan IFormatter. En aquella ocasión tuvimos la oportunidad de ver cómo definir los tipos que queríamos serializar por medio del atributo Serializable y la interfaz ISerializable. En este artículo veremos otra forma de serializar nuestros objetos, que utiliza XML puro como formato.

Serialización XML La serialización XML nos permite serializar las propiedades y campos públicos de los tipos, y aunque pueda parecer que tiene menos "potencia" que la serialización binaria, ya que ésta última nos permite serializar el objeto completo, seguramente será la que más utilicemos, principalmente por el hecho de que está basada en el estándar XML, lo que nos permite el intercambio de datos entre diferentes plataformas. Utilizando este tipo de serialización, no tendremos límites a la hora de distribuir nuestros objetos, ya que XML es un estándar, y por tanto cualquier lenguaje o plataforma que hable ese estándar nos comprenderá. La clase principal para la serialización XML es XmlSerializer (definida en el espacio de nombres System.Xml.Serialization), y para realizar las operaciones de serialización y deserialización se usan los métodos Serialize y Deserialize. Como vemos, esto es parecido a lo que vimos en el artículo anterior, pero la diferencia principal es que esta clase no requiere que las clases estén marcadas con el atributo Serializable; los únicos requisitos de las clases a serializar es que sean públicas, tengan un constructor público sin parámetros y, por supuesto, que tengan elementos públicos que se puedan almacenar en un fichero. La forma más simple de usar esta clase es indicando en el constructor el tipo que queremos serializar, y las operaciones de serialización se realizarán en un fichero cuyo formato será XML. Accederemos a ese fichero por medio de clases de tipo Stream. Por ejemplo, si tenemos una clase llamada Colega2, podemos usar el código mostrado en el listado 1 para realizar la serialización y posterior deserialización. El fichero resultante al realizar las operaciones del código del listado 1 se muestra en el listado 2. Requisitos de los tipos a usar
con XmlSerializer Tal como comenté antes, las clases que podemos serializar por medio de XmlSerializer no están "obligadas" a usar atributos especiales, pero sí deben tener ciertas características, como un constructor público sin parámetros. Este detalle solo debe preocuparnos si hemos definido en la clase algún constructor con parámetros, ya que el único caso en el que es el propio compilador el que genera ese constructor predeterminado (sin parámetros) es cuando nosotros no definimos ninguno; por tanto, debemos asegurarnos de que la clase tiene un constructor público y sin parámetros. Además, la clase debe estar definida como pública. En el código del listado 3 tenemos la definición de la clase Colega2 que hemos usado en el ejemplo anterior, en la que además del constructor sin parámetros también se ha definido otro que recibe dos parámetros. Si la clase que definimos es de tipo colección, es decir, implementa la interfaz IEnumerable o ICollection, también debe cumplir otros requisitos además de los antes comentados. Uno de ellos es que si nuestra clase implementa IEnumerable (o ICollection, ya que ICollection implementa IEnumerable) debe implementar un método Add que sea público y que reciba un único parámetro del mismo tipo que el devuelto por el método GetEnumerator, que en realidad será (o deberá ser) el tipo de datos que la colección va a mantener. Por ejemplo, si vamos a crear una clase que sea una colección del tipo Colega2 mostrado en el listado 3, el parámetro del método Add debe ser de ese tipo o de cualquier clase en la que éste se base. En nuestro ejemplo, el tipo Colega2 no se deriva explícitamente de ningún otro; pero al igual que todas las clases de .NET, desciende de Object, y por tanto el parámetro del método Add puede ser Object o Colega2. Si nuestra clase implementa la interfaz ICollection (o se deriva de cualquier clase que la implemente), debe además tener un indizador (propiedad predeterminada en Visual Basic) que reciba un parámetro de tipo entero y devuelva el mismo tipo que el usado en el método Add. Además, debe definir la propiedad Count (aunque esto último es por "exigencias" de la propia interfaz); esa propiedad puede ser pública o estar implementada explícitamente, aunque es mejor definirla como pública, ya que Count es algo que todo el mundo espera encontrar en una colección. Cuando se va a serializar el objeto, se comprueba por medio de Count el número de elementos que tiene y para obtener cada uno de los elementos de la colección se usa el indizador. A la hora de deserializar el objeto, se obtienen los elementos de la colección por medio de GetEnumerator. En el listado 4 tenemos la definición de una clase que implementa ICollection y que mantendrá una colección del tipo Colega2. Como vemos, y por razones de rendimiento, internamente utiliza una colección genérica de tipo List<Colega2>. La forma de serializar la colección Colegas2 es como vimos en el listado 1, pero usando Colegas2 en lugar de Colega2 como tipo de datos (elemental, querido Guille). El resultado obtenido al serializar esa clase será parecido al código XML mostrado en el listado 5. XmlSerializer no puede serializar directamente colecciones de los tipos ArrayList o List<T>; lo que sí podemos hacer es usar internamente esos tipos, aunque en ese caso, el tipo expuesto por nuestra clase debe ser alguno que esa clase pueda serializar. En el código que acompaña al artículo hay un ejemplo parecido al usado en el código del listado 5, en el que internamente se usa un array de tipo List<Colega2>. Otro tipo de datos común que tampoco podrá serializar esta clase son las colecciones de tipo diccionario, ya sean normales o genéricas; en general, cualquier clase que implemente IDictionary no se puede serializar con esta clase.

Aplicar atributos para
controlar el XML generado
al serializar Cuando usamos la clase XmlSerializer para guardar en un fichero el contenido de un objeto, el formato de ese fichero es XML. Esto ya quedó claro desde el principio, y debido a que, entre otras cosas por falta de espacio, no vamos a hablar en este artículo de qué es XML, al menos debemos tener ciertas nociones sobre el formato usado, ya que eso nos permitirá tener mayor control sobre el fichero resultante al serializar un objeto.
Por ejemplo, si vemos el listado 2 o el listado 5, comprobaremos que los datos se guardan como elementos, y siempre debe haber un elemento raíz. En el caso del listado 5, el elemento raíz es <ArrayOfColega2>. Ese elemento raíz contiene el resto de elementos; siguiendo con el mismo ejemplo, hay dos elementos contenidos en el raíz (<Colega2>). Esos elementos, a su vez, contienen otros elementos para cada uno de los datos a serializar. De forma predeterminada, la clase XmlSerializer crea "elementos" de cada clase que serializa, y también crea elementos para cada miembro público (campo o propiedad) que la clase defina. Los nombres usados (salvo en el caso de los arrays o colecciones) son los nombres que nosotros hemos definido en la clase.
Mediante atributos podemos cambiar la forma predeterminada de actuar. Por ejemplo, podemos indicar que ciertos campos o propiedades no se serialicen o que en lugar de utilizar un elemento XML para un cierto miembro de la clase se cree un atributo. También podemos indicar qué nombre queremos que se utilice y algunas otras cosas más; empecemos por las más importantes. Para no tener en cuenta un campo o propiedad, usaremos el atributo XmlIgnore aplicado a dicho campo o propiedad. Por ejemplo, si añadimos a la clase Colega2 un campo para la Fecha, podemos evitar que se serialice definiéndolo de esta forma:

[XmlIgnore] public DateTime Fecha;

Si queremos usar un nombre distinto para un elemento (para que no se use el mismo nombre del campo o propiedad), usaremos el atributo XmlElement indicando en la propiedad ElementName el nombre que queremos usar o bien indicando el nombre directamente, ya que otro de los constructores de este atributo admite una cadena para el nombre a asignar. Por ejemplo, si queremos que el campo Email se serialice como Correo, usaremos el siguiente código:

[XmlElement(ElementName="Correo")] public string Email;

Y si en lugar de crear un elemento, queremos que se almacene como un atributo (los atributos se guardan dentro del elemento que contiene la clase a almacenar), debemos aplicar el atributo XmlAttribute. Por ejemplo, para que el campo Nombre de la clase Colega2 se guarde como un atributo, haremos lo siguiente:

[XmlAttribute] public string Nombre;

También podemos aplicar el atributo XmlRoot a la definición de la clase para, por ejemplo, usar un nombre diferente. Con el siguiente código, la clase Colega2 se guardará como MiColega:

[XmlRoot("MiColega")] public class Colega2 { // ... Por supuesto, aunque cambiemos la forma en que se almacenará la clase, el tipo sigue siendo el mismo. Haciendo estos cambios a nuestra clase y usando el mismo código del listado 1, el fichero resultante sería como el mostrado en el listado 6, en el que debemos notar que ahora el campo Email se almacena como Correo y que el campo Nombre se guarda como un atributo del elemento que define la clase, que al haberle cambiado el nombre será <MiColega>. Como podemos imaginar, el atributo XmlAttribute también permite que cambiemos el nombre con el que queremos que se guarde el campo o propiedad en el que lo aplicamos. Además de cambiar el nombre o si se almacenará como elemento o como atributo, por medio de XmlAttribute y XmlElement podemos indicar el tipo de datos con el que queremos que se guarde en el fichero XML. Esto nos será de utilidad si queremos que uno de nuestros campos (o propiedades) se exporten como otro tipo de datos que no defina el propio .NET, pero que sí esté definido por el consorcio W3C. Estas asignaciones las haremos por medio de la propiedad DataType de los dos atributos mencionados, y el valor que asignemos a esa propiedad debe coincidir exactamente con el definido por el W3C (recordemos además que XML es un lenguaje que distingue las mayúsculas de las minúsculas). Por ejemplo, si en la clase Colega2 queremos añadir un campo que se exporte como si fuese del tipo token, tendríamos que definirlo de la siguiente forma:

[XmlElement(DataType = "token")] public string ID;

Aunque en el fichero resultante no veremos nada que indique el tipo, y de hecho se guardará como cualquier otro valor. Y es que en realidad ese tipo de datos que indicamos servirá para el tipo de datos generado como un esquema XSD (XML Schema Definition, definición de esquema XML). Crear esquemas
con la utilidad xsd.exe Si los datos que queremos manejar no son totalmente compatibles con .NET o queremos que los datos que hemos serializado se utilicen desde otras plataformas, será conveniente que esos datos se ciñan a ciertas características o que exportemos el esquema usado para serializar nuestras clases. La utilidad Xsd.exe, que se incluye con el SDK de .NET, nos permite crear esquemas XSD a partir de ensamblados (DLL o EXE), crear clases a partir de esquemas XSD e incluso crear esquemas a partir de un fichero XML que contiene los datos de un objeto serializado. Esta utilidad la usaremos desde la línea de comandos, y como primer argumento suministraremos el fichero que queremos procesar. Opcionalmente, podemos indicar el nombre del fichero de salida y algunas opciones adicionales para cambiar el comportamiento predeterminado, que es el de crear esquemas XSD a partir del fichero que indiquemos en el primer argumento, al menos si ese fichero tiene extensión .DLL, .EXE o .XML; de ser así, se generará el esquema XSD, un fichero con la extensión .XSD. Si procesamos un fichero XML, el nombre que se le dará al esquema será el mismo que tiene el fichero, pero con la extensión .XSD. Si generamos esquemas a partir de ensamblados, el nombre que se le dará al fichero resultante será schema0.xsd. Si queremos crear el esquema a partir del fichero XML con el resultado del código que hemos estado usando, tendríamos que escribir lo siguiente en la línea de comandos (por ejemplo, con el acceso directo que crea Visual Studio 2005):

xsd Colega03.xml

Los tipos de datos XML que la utilidad usará serán los que "crea" que son los adecuados. Por ejemplo, el tipo ID lo hemos indicado como tipo token, pero como en el fichero .XML no se indica de que tipo será, la utilidad XSD lo exportará como de tipo string. En el listado 7 tenemos el fichero XSD generado con la línea de comandos anterior, en el que podemos observar que se crea un tipo DataSet que no se incluye en el fichero procesado; pero como XSD no sabe el tipo real de MiColega, asume que es un tipo complejo y lo exporta con el atributo IsDataSet; esto mismo hará con las colecciones, aunque en realidad el tipo que genera es un array del tipo al que hace referencia, en este caso del tipo MiColega. Para tener un fichero de esquemas más preciso, tendremos que generarlo a partir del ensamblado en el que tenemos definida la clase. Por ejemplo, si el código que hemos estado usando está compilado como SerializacionXML03_cs.exe, podemos crear el esquema de la siguiente forma:

xsd SerializacionXML03_cs.exe Y el fichero resultante tendrá un aspecto parecido al mostrado en el listado 8. En este caso, se conoce cuál es el tipo de datos de MiColega y el tipo del campo ID se indica como el que definimos por medio de la propiedad DataType del atributo XmlElement. Esta utilidad también nos sirve para generar clases a partir de un esquema XSD, con la idea de que podamos usar dicha clase en nuestro proyecto para poder acceder correctamente a los datos serializados, por ejemplo, desde otra plataforma o por otros lenguajes. Si usamos el esquema mostrado en el listado 8 y queremos generar una clase, lo haremos escribiendo lo siguiente:

xsd schema0.xsd -c

Esto generará una clase de C#; si quisiéramos que se genere una de Visual Basic, debemos indicarlo después de la opción /L:

xsd schema0.xsd -c -l:VB

Los lenguajes soportados son 'CS' para C#, 'VB' para Visual Basic, 'JS' para JScript y 'VJS' para J#. De forma predeterminada, se crearán propiedades para cada uno de los elementos de la clase, pero si queremos que se generen como campos, debemos usar la opción /f:

xsd schema0.xsd -c -f

La clase generada a partir de este último comando es la mostrada en el listado 9, de la que he quitado parte del código que indica que es una clase autogenerada. Volviendo al tema del tipo DataSet (que no es un DataSet sino un array) que genera la utilidad cuando procesa un fichero con extensión .XML, en realidad lo que hace es crear un array del tipo de datos que tiene ese fichero. Por ejemplo, si generamos una clase a partir del esquema del listado 7, además de la definición de la clase MiColega se creará también un "tipo" llamado NewDataSet que en el fondo es un array del tipo MiColega, tal como podemos ver en el listado 10. Independientemente de lo que aparente ser, lo que debe importarnos es que con la clase generada a partir de un esquema (aunque ese esquema lo hayamos obtenido a partir de un fichero de datos XML), podremos acceder a los datos que se han serializado, incluso aunque se haya hecho con cualquier otro lenguaje o sistema operativo. Para demostrar esto último, en el ZIP con el código de ejemplo se incluye un fichero de datos XML con el que el lector puede practicar lo aquí comentado; para comprobar cómo usarlo, también se incluye un proyecto que gracias a la clase generada por la utilidad xsd.exe es capaz de acceder a esos datos. Aunque mi recomendación es que intente primero generar el proyecto y después compararlo con el que yo he hecho. Como sugerencia, puede modificar la clase generada para que tenga un método ToString, de forma que sea fácil mostrar el contenido de cada objeto.

Conclusiones Como hemos podido comprobar, la serialización XML es bastante potente, y por consiguiente muy extensa como para poder tratarla al completo en un artículo, pero confío que lo aquí explicado sea más que suficiente para que pueda sacarle provecho. Recuerde además que este tipo de serialización es el que utilizan los servicios Web o las aplicaciones de acceso a datos. Como de costumbre, en el ZIP que acompaña a este artículo está todo el código aquí presentado, tanto para Visual Basic como para C#.

blog comments powered by Disqus
autor
referencias