Guía de migración y actualización a
 Visual Basic .NET

Conozca en su propio idioma todo lo que la plataforma .NET y VB tienen para Usted

Guía de migración y actualización a VB .Net 

 

Persistencia y serialización XML en .NET framework (Erich Bühler, www.vblibros.com. Publicado en revista Sólo programadores, marzo 2004)
Fuentes clic aquí

En este artículo se analiza de forma extensa las características de persistencia en .NET framework, así como también las técnicas de serialización XML y binaria. Estas últimas ofrecen un nuevo conjunto de posibilidades altamente aplicables a la mayor parte de las aplicaciones.
 

Persistencia y serialización XML en .NET framework

Todos los lenguajes utilizan algún tipo de persistencia, esto es, almacenar información en algún medio o de alguna forma como ser un archivo o base de datos. Normalmente ofrecen este soporte desde hace ya décadas, aunque ha habido grandes cambios en los últimos tiempos lo que implica que sea necesario dar un nuevo vistazo a estas funcionalidades. Si tuvo la oportunidad de leer mi artículo del mes anterior habrá conocido XML y como gestionar este tipo de formato haciendo uso de las características de .NET framework. Actualmente es importante este tema ya que alrededor del 96% de las aplicaciones guardan los datos en alguna parte, por lo que si desconoce como llevar adelante esta tarea se quedará tan sólo con un 4% del mercado, cosa más que insuficiente para cualquiera.

En .NET framework y a grandes rasgos existen dos formas de efectuar la persistencia de datos, la primera es utilizando una base de datos, mientras que la segunda es usando archivos, búferes de memoria y hasta información enviada a través de una conexión de red. Vamos a obviar en este artículo el acceso a bases de datos, o lo que es igual, las clases denominadas ADO.NET que son las que implementan y posibilitan esto último. Sin embargo excluyendo ellas queda un panorama muy rico y lleno de cosas interesantes para ver, muchas de las cuales son la base para varias tecnologías de .NET framework.
 

Más sobre persistencia de datos

Es erróneo pensar hoy en día que la persistencia está ligada con datos que se envían o reciben de archivos. Esto era así hasta hace un tiempo atrás donde las funcionalidades de los lenguajes estaban orientadas hacia el uso exclusivo del sistema de archivos de Windows. Sin embargo hoy el panorama es diferente y como consecuencia el espectro de posibilidades es mayor. Si ha trabajado en Java notará que no es nada nuevo lo que le estoy planteando, aunque sí lo es dentro del mundo Microsoft. Tenemos entonces que para lograr este objetivo se cuenta con elementos tales como archivos, búferes de memoria, canales de red, o todo aquello por donde se pueda transmitir información para que sea finalmente guardada o interpretada en algún punto. También los datos puede ser almacenados utilizando varios formatos tales como texto plano o XML, binario y SOAP (este último se explicará al final de este artículo). Esto hace que sea un gran problema para el desarrollador cuando da sus primeros pasos en .NET framework y desea algo tan simple como escribir información en un archivo. En realidad no es que sea complejo pero sí requiere entender plenamente varios conceptos de la infraestructura y su funcionamiento.


Figura 1. Panorama general en .NET framework.

Comencemos entonces por lo más simple que es la gestión de formatos de texto y para ello nos centraremos en las siguientes 2 estructuras del espacio de nombres System.IO:

 

-         TextReader

-         TextWriter

 

Analizando TextReader y TextWriter

TextReader es la clase base para todas las estructuras que acceden a datos de tipo texto de lectura, mientras que TextWriter es la base para todas aquellas que escriben una secuencia de caracteres. Más adelante veremos como gestionar formatos binarios.

Aunque parezca extraño nunca definirá variables de este tipo y el motivo es que son las estructuras base, esto es, en las que se basan otras para implementar tareas de persistencia o acceso a datos. Es importante entender que se parte de estas dos clases para construir las otras, por lo que comenzando por aquí todo se hará más fácil. Vea el cuadro 1 para conocer las clases derivadas y su relación:

Clase base

Clase derivada

Utilidad

TextReader

StreamReader

Lee caracteres o líneas de un archivo.

StringReader

Lee caracteres o líneas de una cadena de texto de tipo String.

TextWriter

StreamWriter

Escribe caracteres o líneas a un archivo de texto.

 

StringWriter

Escribe caracteres a una variable de tipo StringBuilder.

Tabla 1. Clases derivadas de TextReader y TextWriter

Como ve cada una brinda 2 posibilidades de gestión de lectura/escritura y se diferencian principalmente en que mientras una envía los datos a un archivo de texto la otra lo hace a una variable. Comenzaremos por esta última opción que es la más sencilla y luego avanzaremos mostrando algunos ejemplos más complejos con archivos.

La clase StringReader permite acceder al contenido de una variable de tipo String, mientras que StringWriter necesita que el tipo sea StringBuilder. Esta última estructura es en realidad un búfer con datos y como es posible apreciar en el listado 1 es muy fácil convertir de un tipo a otro. El motivo de que no se utilice un tipo String en ambos casos es meramente técnico y principalmente de rendimiento.

Mediante StringReader/StringWriter las cadenas de texto pueden ser modificadas, accedidas de línea a línea o en su totalidad. El siguiente ejemplo inicializa con caracteres una variable llamada MiTexto y luego hace uso de un objeto de tipo StringReader para leer y StringWriter para efectuar modificaciones:

//En la zona de importaciones escribir las 2 líneas de abajo.

using System.IO;
using System.Text;

//Se inicializa el objeto StringBuilder con el siguiente contenido:
StringBuilder MiTexto = new StringBuilder("Esto es una línea\r\n");

//Se crea el lector y escritor. Véase que StringReader utiliza un tipo String
//y por ello debe ser convertido utilizando ToString.
StringReader MiLector = new StringReader(MiTexto.ToString());
StringWriter MiEscritor =  new StringWriter(MiTexto); 

//Exhibe la línea.
Console.WriteLine(MiLector.ReadLine());

//Adiciona una línea.
MiEscritor.WriteLine("Esta es otra línea");

//Muestra ambas líneas.
Console.WriteLine(MiTexto);

Listado 1. 

La utilidad mayor es que se manipulan información de una variable con texto tal cual se haría con las clases que gestionan archivos, esto es, usando un conjunto similar de métodos. Lo he planteado así forma porque de esta forma se evita temporalmente conocer como abrir, cerrar, o enviar a un archivo los datos. Si el valor  de retorno del método ReadLine  fuese nulo (null) entonces se sabría que se ha alcanzado al final. La tabla 2 describe un resumen de los métodos de la clase:

Clase

Método

Descripción

StringReader

Peek

Lee un carácter pero no desplaza el puntero a la próxima posición

NewLine

Lee o configura el carácter terminador de línea.

Read

Lee un carácter y avanza a la próxima posición.

 

ReadLine

Lee la totalidad de la línea.

 

ReadToEnd

Lee desde la posición actual hasta el final.

StringWriter

Write

Escribe uno o más caracteres.

WriteLine

Escribe un conjunto de caracteres y adiciona un terminador de línea.

Tabla 2. Compendio de métodos de las clases derivadas de TextReader, TextWriter

Si bien la manipulación de texto almacenado en una cadena puede ser de gran utilidad, en la mayor parte de los casos necesitará obtener el mismo desde un archivo. Para ello es necesario el empleo de StreamReader/StreamWriter. La siguiente línea muestra como hacer lo mismo pero ahora obteniendo la información de un archivo:

//Se crea el lector.
StreamReader MiLector = new StreamReader("MiArchivo.txt");                    

//Exhibe la línea.
Console.WriteLine(MiLector.ReadLine());

//Cierra el archivo sino al abrirlo de escritura dará error ya que estará bloqueado.
MiLector.Close();

//Abre de escritura, indica que las nuevas líneas deberán ser adicionadas (Append). También especifica formato de caracteres UTF8.
StreamWriter MiEscritor =  new StreamWriter("MiArchivo.txt", true, System.Text.Encoding.UTF7);

//Adiciona una línea.
MiEscritor.WriteLine("¡Esta es otra línea!");

//Cierra el Stream.
MiEscritor.Close();

Listado 3.

*Nota: La lectura o escritura también puede ser asíncrona, lo que posibilita efectuar varias tareas al mismo tiempo. 

Tenga en cuenta que es recomendable especificar el tipo de codificación de los caracteres ya que de lo contrario se utilizará la predeterminada. Esto es sano si se quieren  almacenar o leer correctamente tildes y otros acentos.

Para el caso que el archivo no exista o se produzca algún otro tipo de problema al momento del acceso, entonces automáticamente .NET framework iniciará una excepción que hará posible conocer el motivo. La tabla 3 muestra la lista de posibles excepciones:

Nombre

Descripción

DirectoryNotFoundException

La carpeta no existe.

EndOfStreamException

Se ha sobrepasado el final del contenido.

FileNotFoundException

El archivo no existe.

PathTooLongException

La longitud de la ruta o archivo excede el largo máximo definido por el sistema operativo.

Tabla 3. Lista de excepciones factibles de iniciarse al usar  StreamReader/StreamWriter.

Si le gusta la programación clara le recomiendo que utilice la estructura Try/Catch con el fin de capturar el error y decidir que rumbo deberá tomar el flujo de la aplicación. Tenga en cuenta también que existe la posibilidad de crear una clase propia basada en TextStream, lo que podría ser de utilidad si se desea un mayor control y personalización.

 

Utilización de BinaryReader/BinaryWriter

Si lo que se desea es guardar información de tipos de datos más allá de String entonces la historia es diferente ya que se debe hacer uso de las clases BinaryReader y BinaryWriter. Si bien puede pensarse inicialmente que ellas están basadas o relacionadas con StreamReader/StreamWriter, en realidad son clases independientes que nada tienen que ver con estas últimas. La utilidad principal de BinaryReader/BinaryWriter es escribir tipos de datos de .NET framework a un archivo (decimales, enteros, string, etc). Esta clase se utiliza principalmente cuando se desea guardar un conjunto de valores de variables para recuperarlas más tarde. Veamos algunos métodos y propiedades de las clases:

Clase

Método

Descripción

BinaryReader

ReadBytes

Lee un carácter de tipo byte y avanza a la próxima posición.

ReadChar

Lee un carácter de tipo char y avanza a la próxima posición teniendo en cuenta para esto el tipo de codificación utilizada.

 

ReadDouble

Lee un carácter de tipo char y avanza a la próxima posición de acuerdo al tipo de codificación utilizada.

 

ReadSingle

Lee un carácter de tipo Single y avanza a la próxima posición.

BinaryWriter

Flush

Vacía el búfer intermedio y lo envía al destino.

Seek

Establece la nueva posición de escritura.

 

Write

Escribe un valor teniendo en cuenta su tipo.

Tabla 4. Resumen de los métodos de las clases BinaryReader/BinaryWriter del espacio de nombres System.IO.

 

Utilización de Streams

Imagine que necesita transmitir información entre 2 oficinas lo primero que le recomendaría es montar un cable entre ambas y normalmente el próximo paso sería conectar los periféricos u computadores a este. Ellos serían luego los encargados de enviar o recibir e interpretar la señal. En .NET framework no existe una clase llamada Cable pero sí una llamada Stream que realiza una labor similar. Una clase Stream representa una secuencia de bytes y como ellos deberán ser enviados o recibidos, pero nada de cómo estos tendrán que ser tratados. Son luego las clases derivadas quienes implementan cada caso en particular. Vea la tabla 5 para conocer este mismo número de posibles alternativas:

Nombre y espacio

Descripción

System.IO.BufferedStream

Adiciona un búfer de lectura/escritura para ser utilizado con otro Stream.

System.IO.FileStream

Acceso a un archivo tanto de forma asíncrona como sincrónica.

System.IO.MemoryStream

Crea un búfer en memoria donde gestionar información.

System.Net.Sockets.NetworkStream

Permite gestionar información desde y hacia una red.

System.Security.Cryptography.CryptoStream

Aplica un cifrado a un conjunto de datos.

 Tabla 5.

Figura 2.

Esto permite alterar dinámicamente el medio a través del cual se obtienen los datos o hacia donde se envían. La siguiente línea pertenece al constructor de la clase BinaryReader que aprendimos anteriormente y si presta atención notará que el tipo aceptado es de tipo Stream.

public BinaryReader(Stream input); 

Esto significa que es posible recibir información binaria desde cualquiera de las estructuras especificadas en la tabla 4. En realidad BinaryReader no se entera de donde provienen los mismos, simplemente se remite a leer la información. El listado 3 exhibe como cargar datos binarios desde un archivo de texto:

 

//Abre un Stream de un archivo.
FileStream Archivo = new FileStream("MiArchivo.bin", System.IO.FileMode.Open); 

//El Objeto BinaryReader consume el Stream creado anteriormente.
BinaryReader LectorBinario = new BinaryReader(Archivo); 

//Se imprime el valor a la consola.
Console.Write(LectorBinario.Read());

Listado 3.

El fuente final es bastante sencillo de comprender ahora que se tiene claro la utilización de Stream. Una última cosa importante es que no es posible utilizar con estas técnicas las estructuras StreamReader y StreamWriter ya que ellas no son derivadas de Stream pese a que comienzan con esta palabra.

 

Serialización XML

La serialización es en proceso un tanto complejo que hace factible que muchas de las innovadoras características funcionen y por supuesto abre el abanico a nuevas soluciones y posibilidades. El tener claro los apartados anteriores le será de muchísima utilidad para comprender de que se trata y como funciona este proceso.

La serialización posibilita que el estado de un objeto pueda ser convertido a un documento XML para que el estado pueda ser recuperado más tarde. Esto es, leer los valores de sus propiedades y escribirlas a un archivo con formato XML, para luego en algún momento de la historia de la aplicación crear un nuevo objeto y aplicar el documento al mismo. Esto dará como resultado un nuevo objeto idéntico al inicial. El motivo principal de que exista esta funcionalidad estriba principalmente en que las nuevas aplicaciones se basan en objetos interactuando entre sí. Es por ello que se requiere esta funcionalidad como característica básica para guardar el contenido de los integrantes de la aplicación. Pero… ¿Qué otra utilidad tiene la serialización?

Bien, voy a volver al ejemplo del artículo anterior que me parece bastante simple y claro. Seguramente haya visto en algún momento la serie televisiva Star Trek, bueno… si no la vio a ciencia cierta sabrá de qué se trata. Imagínese que se necesita transferir de un punto de la galaxia a otro que está a millones de años luz, normalmente no elegirá hacer uso de los medios tradicionales sino que optará por algo mucho más rápido como es la tele-transportación. A mi me gusta mucho ya que puedo trasladarme de un sitio a otro en cuestión de segundos sin importar la distancia y hasta sumo Puntos luz que puedo luego canjear por descuentos en comercios y futuras teletrasportaciones. La idea es sencilla, simplemente me introduzco dentro de una especie de tubo con puerta y luego marco las coordenadas espaciales hacia donde quiero dirigirme. Si todo sale bien en cuestión de segundos estoy allí. ¿Cómo se lleva esto acabo?

Básicamente mi persona es dividida en trozos y enviada en orden a través de éter, o también podría ser adicionada información de como ensamblar las partes correctamente. Lógicamente esto daría como resultado a mi persona tal cual era originalmente. De omitirse esta última información quedaría finalmente al buen estilo de Salvador Dalí.

Si bien Microsoft no puede todavía teletransportar personas, si puede hacerlo con objetos dentro del mundo .NET framework. Dentro de la jerga de la infraestructura .NET al proceso de tele-transportación se lo denomina publicación o serialización XML, y es la clave de muchas de las tecnologías como ser Servicios Web XML, objetos DataReader con información, etc.

 

Figura 3.

Durante el proceso básicamente se tiene un objeto con valores en sus propiedades los que se leen y configuran a un documento XML. Este podría ser posteriormente enviado a través de la red (Internet, Intranet, etc.) con el fin de que sea recibido por alguien en algún punto. En destino otra aplicación con la ayuda de .NET framework podría leer el documento y recuperar el estado del objeto. Como resultado se obtendría una copia del objeto inicial en otro punto. Existen 3 posibles formatos a utilizar para almacenar la información durante este proceso (vea la tabla 6):

Nombre

Descripción

Formato

A tener en cuenta…

XmlSerializer

Serializa los valores de una clase. Si hay una propiedad que contiene una referencia a otra clase dará error al serializar.

XML

Es texto y por ello puede ser entendido por cualquier sistema operativo y transmitido o guardado sobre cualquier medio. Solamente se serializan las propiedades públicas de los objetos.

BinaryFormatter

Serializa los valores de una clase. Si hay una propiedad que contiene una referencia a otra clase, serializará también esta última.

Binario

Optimo para lograr un tamaño pequeño fácil de gestionar en múltiples medios.

SoapFormatter

Serializa los valores de una clase. Si hay una propiedad que contiene una referencia a otra clase, serializará también esta última.

SOAP

Optimo para el uso en tecnologías de invocación remota RPC (Remote procedure call).

Tabla 6. Tipos de formato de serialización.

Es importante el formato si se enfatiza en la velocidad y tamaño o lo que interesa es la compatibilidad con otros sistemas operativos y/o protocolos de transferencia. Para definir una variable de tipo XmSerializer es necesario acceder al espacio System.Xml.Serialization, mientras que la clase BinaryFormatter se encuentra en System.Runtime.Serialization.Formatters.Binary. Por su parte el encargado de generar SOAP se puede localizar en System.Runtime.Serialization.Formatters.Soap.

Figura 4. Debe adicionar una referencia a alguna de estas bibliotecas para utilizar las clases de formato binario y SOAP.

 

El siguiente ejemplo (listado 4) crea un archivo binario con el contenido de una estructura de tabla Hash y luego la recupera y exhibe.

 

using System.IO;

using System.Runtime.Serialization.Formatters.Binary;static void Serializar()

{

       //Crea una tabla Hash con valores a serializar.
       Hashtable Clientes = new Hashtable();
       Clientes.Add("Arnoldo Bühler", "Avda. Aconcagua 4867/202");
       Clientes.Add("Jorge", "Avda. Italia 20 / 3B "); 

       //Abre un stream donde se guardará la serialización.
       //En este caso se almacenará en un archivo
      
FileStream fs = new FileStream("MiArchivo.bin", FileMode.Create);

       //Construye un objeto BinaryFormatter y usa este para serializar a la tabla Hash.
       BinaryFormatter formato = new BinaryFormatter();
       formato.Serialize(fs, Clientes);             

       fs.Close();

static void Deserealizar()
{
       //Declara una tabla Hash vacía.
       Hashtable Clientes  = null

       //Abre el archivo conteniendo la información del objeto serializado anteriormente.
      
FileStream fs = new FileStream("MiArchivo.bin", FileMode.Open); 

       //Abre un stream de donde se obtendrá la serialización.
       BinaryFormatter formato = new BinaryFormatter(); 

       //Deserializa la tabla Hash, por lo que se obtiene nuevamente
       //el objeto. Se debe convertir el objeto inicial al tipo HashTable.
      
Clientes = (Hashtable) formato.Deserialize(fs); 

       fs.Close();
}

Listado 4. 

Preste atención a la utilización de los métodos Serialize y Deserealize que es donde todo ocurre. Básicamente al invocar el método Serialize se debe especificar el Stream que es hacia donde se enviará la información. La deserialización es aun más sencilla ya que simplemente hay que asignar el resultado de la función al objeto, previo convertir este al tipo esperado.

Con unos pequeños cambios se podría adaptar para que el listado final trabajase en formato SOAP o XML en vez de binario. Veamos ahora otro ejemplo aun más interesante, aquí se utiliza una estructura personalizada en vez de la tabla Hash. Para dicho fin crearemos una clase llamada Caja que contendrá varias propiedades (vea el listado 5):

[Serializable()]

public class Caja
{
      
public double Peso;
      
public double Volumen;
       private string Contenido = "Relojes de arena marca Arenux"; 

       public Caja()
       {
              // Constructor predeterminado de la clase
      
}
}

Listado 5. 

Como podrá apreciar la clase no tiene nada de particular salvo que se incluye la etiqueta Serializable al comienzo de la misma. Esto es indispensable para informarle al compilador que dicha estructura es factible de ser serializada, de lo contrario al intentar llevar adelante esta tarea se obtendrá un mensaje de error indicando que la clase no ha sido marcada para esta funcionalidad. El listado 6 muestra la implementación de las funciones que encarnan el proceso:

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
static void Serializar()
{
       Caja MiCaja = new Caja(); 
 

       //Configura el peso y volumen.
       MiCaja.Peso = 10;
       MiCaja.Volumen = 123.30; 

       //Serializa la caja...
      
FileStream fs = new FileStream("MiCaja.bin", FileMode.Create);
      
BinaryFormatter formato = new BinaryFormatter();
       formato.Serialize(fs, MiCaja); 

       fs.Close();

static void Deserealizar()
{
       //Declara una tabla caja vacía.
       Caja MiCaja  = null;
 

//Deserealiza al objeto de tipo Caja.
       FileStream fs = new FileStream("Micaja.bin", FileMode.Open);
      
BinaryFormatter formato = new BinaryFormatter();
       MiCaja = (Caja) formato.Deserialize(fs); 

       //Exhibe los valores (10 y 123,30 respectivamente).
       Console.WriteLine(MiCaja.Peso);
      
Console.WriteLine(MiCaja.Volumen);
       fs.Close(); 
}

Listado 6.

Es más sencillo comprender mandarín que el binario de la figura 4 que es el generado por esta aplicación. Afortunadamente no es necesario entender al mismo ya que siempre se gestionará mediante los métodos vistos anteriormente.

 

Figura 5. Vista del resultado binario. 

Para que se genere formato XML son pocos los cambios que hay que hacer al fuente, sin embargo se deben conocer algunas cosas importantes como que haciendo uso de este formato no se almacenan las propiedades marcadas como privadas y se producirá un error si alguna de estas contiene una referencia a otra clase o estructura. El listado 7 exhibe solamente la parte que cambia, con el fin de no tener que incluir nuevamente todo el código fuentes:

//Serialización.
FileStream fs = new FileStream("MiCaja.xml", FileMode.Create);
XmlSerializer formato = new XmlSerializer(MiCaja.GetType()); 

//Deserialización.
FileStream fs = new FileStream("Micaja.xml", FileMode.Open);
XmlSerializer formato = new XmlSerializer(MiCaja.GetType());

Listado 7. 

Una diferencia importante es que se debe adicionar al constructor de la clase la información de la estructura que se va a serializar, esto se hace mediante la utilización del miembro GetType. El resto son modificaciones estéticas como ser el cambio del nombre de extensión a XML para que se asocie automáticamente el tipo en el explorador de Windows.  Vea el resultado en el listado 8.

<?xml version="1.0" ?>
<Caja xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<Peso>10</Peso>

<Volumen>123.3</Volumen>

</Caja>

Listado 8. 

Otra funcionalidad interesante es la posibilidad de incluir la etiqueta [NonSerialized()] al comienzo de una propiedad para indicar que dicho elemento deberá excluirse de la serialización. Esto es aplicable cuando se desea ser selectivo sobre la información a guardar.

Ha sido un artículo extenso y espero haber cubierto sus dudas sobre como es posible gestionar persistencia de diferentes tipos de dato así como también la serialización de objetos. De ser así entonces el objetivo estará cumplido.

Nota de copyright: Todas las marcas y/o tecnologías aquí citadas, son marcas registradas de las respectivas empresas y/o dueños.

© 2004 InetWork
All right reserved