Serialização e desserialização de Objectos em C#

Serializador costumizado

Preparação dos objectos.

Porque pretendemos serializar os objectos de forma automática para um formato costumizado, temos que preparar os objectos para essa tarefa. O .Net tem um conjunto de atributos que devemos adicionar ao código dos objectos para tornar a construção e desconstrução do objecto em XML possível. Vamos primeiro analisar a classe Contacto:

public class Contacto
{
  public Contacto() { }
 
        public Contacto(string contacto, string tdc)
        {
            this.Valor = contacto;
            this.Tipo = tdc;
        }
 
        private string _valor;
        [XmlElement]
        public string Valor
        {
            get { return _valor; }
            set { _valor = value; }
        }
 
        private string _tipo;
        [XmlAttribute]
        public string Tipo
        {
            get { return _tipo; }
            set { _tipo = value; }
        }
 }

A classe tem dois construtores, um vazio (necessário para o serializador) e outro parametrizado. Contém também as propriedades, com membros públicos e privados. O atributo essencial para o processo de serialização é o que está declarado imediatamente antes do membro público. [XmlElement] antes de Valor, indica que valor deve ser serializado como sendo um elemento XML; [XmlAttribute] antes de Tipo indica que Tipo será um atributo do elemento Contacto. É importante notar que os tipos nuláveis (ainda) não são serializáveis como XmlAttribute. O objecto serializado terá a seguinte estrutura:

<Contacto Tipo="telemovel">
   <Valor>912345678</Valor>
</Contacto>

ContactoList, que é declarado como:

[XmlType(TypeName="Contactos")]
public class ContactoList : List<Contacto>
{
}

Será serializado com a forma:

<Contactos>
   <Contacto Tipo="telemovel">
      <Valor>912345678</Valor>
   </Contacto>
   <Contacto Tipo="email">
      <Valor>alho@miguelalho.com</Valor>
   </Contacto>
   (…)
</Contactos>

Por defeito, as listas, sem qualquer modificador no seu atributo, são serializadas como ArrayOfTipo (no caso de ContactoList seria ArrayOfContacto). A utilização do atributo XMLType com a propriedade TypeName sobre a classe permite alterar o nome com que é serializado. O mesmo é possível com o XmlElement através do parâmetro ElementName.

A classe Pessoa e PessoaList são:

public class Pessoa
{
        /// <summary>
        /// construtor vazio é necessário para a serialização
        /// </summary>
        public Pessoa() { }
 
        public Pessoa(string nome)
        {
            this.Nome = nome;
        }
 
        /// <summary>
        /// id é nullo quando se adiocioan o contacto, logo usemos um elemento nulável
        /// </summary>
        private int? _id = null;
        [XmlElement(IsNullable=true)]
        public int? ID
        {
            get
            {
                if (_id.HasValue) return _id.Value;
                else return null;
            }
            set
            {
                if (value.HasValue) _id = value.Value;
                else _id = null;
            }
        }
 
        private string _nome = String.Empty;
        [XmlElement]
        public string Nome
        {
            get { return _nome; }
            set { _nome = value; }
        }
 
        private ContactoList _contactos = null;
        [XmlArray(IsNullable=true)]
        public ContactoList Contactos
        {
            get { return _contactos; }
            set { _contactos = value; }
        }
    }
 
    [XmlType(TypeName = "Pessoas")]
    public class PessoaList : List<Pessoa>
    {
 
    }
O Visual Studio tem um atalho para acelerar a criação das propriedades – escrever “prop” e pressionar a tecla tab duas vezes. Esta acção automaticamente gera o código típico de uma propriedade, incluindo os membros públicos e privado e os get e set do membro público.
Serialização em C#: criação de propriedades no VS
Resta substituir os campos a verde com os nomes desejados. O IDE corrige os get e set automaticamente. Apenas nos casos de usar tipos nuláveis é que é necessário escrever um pouco mais de código, para verificar se o membro tem valor e retornar o valor ou nulo.
public int? ID
{
      get
      {
      	if (_id.HasValue) return _id.Value;
            else return null;
      }
      set
      {
            if (value.HasValue) _id = value.Value;
            else _id = null;
      }
}

Aqui, a primeira novidade é o uso de [XmlArray] para indicar que Contactos (do tipo ContactoList) é efectivamente uma lista e deve ser apresentado no XML como um array de Contacto. A segunda novidade é o IsNullable no atributo de [XmlElemento] da propriedade ID e no [XmlArray] . Caso seja nulo, no XML gerado será escrito na tag xsi:nil="true" indicando explicitamente que é nulo. Sem o IsNullable, no caso de o valor da propriedade ser nulo, o respectivo XML é ignorado. Caso haja algum elemento público que não desejamos passar para o XML (como um método que retorna um valor baseado nas propriedades) podemos usar o [XmlIgnore].

Usando o programa exemplo, se adicionar uma Pessoa (instanciar uma pessoa com o nome preenchido), o XML gerado é:

<Pessoas xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Pessoa>
      <ID>1</ID>
      <Nome>Miguel Alho</Nome>
      <Contactos xsi:nil=”true”/>
   </Pessoa>
</Pessoas>

A informação do namespace (xmlns:xsi…) é adicionada automaticamente pelo serializador e apenas no primeiro elemento. Se adicionarmos mais pessoas e contactos, temos:

<Pessoas xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Pessoa>
      <ID>1</ID>
      <Nome>Miguel Alho</Nome>
      <Contactos>
         <Contacto Tipo="telemovel">
            <Valor>912345678</Valor>
         </Contacto>
         <Contacto Tipo="email">
            <Valor>alho@migeulalho.com</Valor>
         </Contacto>
      </Contactos>
   </Pessoa>
   <Pessoa>
      <ID>2</ID>
      <Nome>outra pessoa</Nome>
      <Contactos>
         <Contacto Tipo="email">
            <Valor>outro@email</Valor>
         </Contacto>
      </Contactos>
   </Pessoa>
</Pessoas>

Os objectos que são arrays são correctamente convertidos para listas de objectos, as propriedades são correctamente serializadas e apresentadas da forma que escolhi. Resta ver então como fazer a serialização, propriamente.

XmlCustSerializer

A classe XmlCustSerializer é que efectua a conversão de e para XML. Está incluída no namespace dos BO para poder ser usada nas diversas camadas da aplicação. É escrita na forma de classe genérica de modo a suportar qualquer tipo que definimos.

public class XmlCustSerializer<T>
{
public static XmlElement ToXml(T obj) //XmlDocument xd) {
    	{
        //cria stream na memória para suportar o Xml
        MemoryStream memoryStream = new MemoryStream();
 
        try
        {
            //Instancia o serializador
            XmlSerializer xs = new XmlSerializer(obj.GetType());
            //Instância o XmlTextWriter
            XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);
            //Serializa os dados da classe para o writer
            xs.Serialize(xmlTextWriter, obj);
 
            //Copia a stream do TextWriter para a memoria
            memoryStream = (MemoryStream)xmlTextWriter.BaseStream;
            //Coloca o apontador para o inicio da stream 
            //(necessário para evitar erros no carregamento dos XMlDoc)
            memoryStream.Position = 0;
            //Instancia o XmlDocument
            XmlDocument xd = new XmlDocument();
            //carrega o Stream em XMl para o XmlDocument
            xd.Load(memoryStream);
            //Obtem o nó raiz com os dados a retornar
            XmlElement node = xd.DocumentElement;
            //retorna o node raíz.
            return node;
        }
        catch
        {
            throw new InvalidCastException("Ocorreu um erro na serialização do objecto (1009002001)");
        }
        //return registo;
    }

O ToXml recebe um objecto do tipo T e converte-o em XML, consoante os atributos da classe. O processo requer a serialização para uma stream de memória através de um XmlTextWriter. Posteriormente o stream é lido para um XMLDocument e o elemento XML é retornado. Como é possível verificar, o XmlSerializer efectua a serialização de forma automática, sem código especifico a determinado objecto.

    public static T FromXml(XmlNode O_Xml)
    {
        //Instancia uma representação de encoding UTF-8
        UTF8Encoding encoding = new UTF8Encoding();
 
        try
        {
            //Instancia uma Stream de memória, para onde é passo o texto Xml do parâmetro de entrada
            MemoryStream memoryStream = new MemoryStream(encoding.GetBytes(O_Xml.OuterXml));
            //retorna o apontador da stream para o inicio
            memoryStream.Position = 0;
            //instância a serialização para o tipo de dados
            XmlSerializer xs = new XmlSerializer(typeof(T));
 
            T obj;
            obj = (T)xs.Deserialize(memoryStream);
 
            return obj;
        }
        catch
        {
            throw new InvalidOperationException("Ocorreu um erro na desserialização do objecto (1009002002)");
        }
    }
}

O método FromXML(XmlNode) efectua o processo inverso – a partir de um nó XML, com o encoding em UTF-8 (possivelmente o método poderá ser alterado para aceitar o encoding como parâmetro), transfere-o para a stream de memória, desserializa-o para o objecto do tipo T e retorna o objecto com os dados.

Para o usar, basta usar a classe para o tipo desejado, por exemplo:

private void ShowXML()
{
 textBox3.Text = XmlCustSerializer<PessoaList>.ToXml(aLista).OuterXml;
}

onde aLista é do tipo PessoaList, e o uso de .OuterXml permite obter uma string. Porque os métodos são estáticos, não é necessário criar uma instância da classe XmlCustSerializer. No caso da desserialização, temos:

private void button3_Click(object sender, EventArgs e)
{
 XmlDocument xDoc = new XmlDocument();
 xDoc.LoadXml(textBox3.Text);
 
 dataGridView1.DataSource = XmlCustSerializer<PessoaList>.FromXml(xDoc.FirstChild);
}

Na aplicação, o XML contido na area de texto é transformado e XmlDocument e desserializado para nova PessoaList, que serve de data source ao dataGridView. Neste exemplo, a serialização e desserialização é usado apenas para apresentar dados, mas podia ser usado para ler os dados de um ficheiro ou escrever para um ficheiro, por exemplo, tornando a conversão automática.

Pode funcionar como exercício, para interessados, modificar a aplicação de modo a que, ao pressionar o botão de adicionar, o objecto pessoa (ou a lista de pessoas) seja escrita para um ficheiro XML (utilizando a camada da DAL), e o processo inverso seja feita da leitura do ficheiro XML para objectos (onde a DAL devolve um PessoaList).

O processo também é bastante permissivo, no sentido que se retirar elementos do XML, posso construir o objecto na mesma (desde que a propriedade permita o nulo por defeito). Por exemplo:

<Pessoas >
   <Pessoa>
      <Nome>Miguel Alho</Nome>
   </Pessoa>
</Pessoas>

é correctamente reconstruído como objecto PessoaList, em que as propriedades ID e Contactos da única Pessoa na lista são nulos. Esta característica pode ser muito útil na definição de um formato de ficheiro que uma aplicação deva receber, e que não necessite de ter os dados completos (diminuindo o tamanho do ficheiro). Depois, após recepção, o objecto desserializado é processado e os campos nulos poderão ser validados ou preenchidos pela aplicação.

Da mesma forma que pode haver ausência de tags, pode existir tags e atributos redundantes:

<Pessoas data="today" origem="a">
   <Pessoa numero="1">
      <Nome>Miguel  Alho</Nome>
      <criado modo="automático"/>
   </Pessoa>
</Pessoas>

Por exemplo os atributos data, origem, e numero, e o elemento criado não são relevantes para a nossa aplicação. No entanto, o objecto PessoaList é construído correctamente, ignorando o que está a mais. Podemos por exemplo guardar o ficheiro num repositório para conservação, mas usar apenas os dados relevantes na aplicação.

Conclusão

Penso que é evidente a utilidade da classe XMLCustSerializer:

  • Implementa o processo de serialização e desserialização de objectos da aplicação para XML de forma automática e customizada.
  • Os mesmos métodos aceitam objectos de tipos customizados, reutilizando código de forma eficiente.
  • A conversão para XML permite implementar métodos mais simples de armazenamento dos dados em ficheiros, e métodos de leitura dos mesmos.
  • O uso de XML é útil para interoperacionalidade da aplicação.

O uso do XML tem custos, no entanto:

  • O overhead das tags é grande, e é sentido em ficheiros com grande quantidade de dados (o XML pode ocupar 10x mais que um CSV equivalente).
    • Uma forma de o contornar é usar atributos onde possível, e utilizar nomes curtos para os elementos.
  • Em comparação aos outros modos de serialização, a binária é mais rápida.

Eu pessoalmente vou utilizando o formato XML, porque se necessário, posso editar um ficheiro gerado à mão, e a conversão directa para objecto facilita a produção de código de armazenamento e leitura dos ficheiros e facilita o processo de declaração do formato de armazenamento.

Bibliografia e Créditos

A classe XmlCustSerializer foi desenvolvida em conjunto com Marco Fernandes e Joaquim Rendeiro.

Algumas fontes usadas para compor a parte teórica do artigo: