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 osget
eset
do membro público.
Resta substituir os campos a verde com os nomes desejados. O IDE corrige osget
eset
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:
- Building Layered Web Applications With Microsoft ASP.NET 2.0, http://Imar.spaanjaars.com
- Introducing Serialization in .NET, Joydip Kanjilal, http://aspalliance.com,
- Serialization in .NET, Nishith Pathak, http://www.CodeProject.com
- XmlSerializer Class, http://msdn2.microsoft.com
- System.Xml.XmlSerializer, Nitin Pande, http://www.eggheadcafe.com