Num projecto recente, desenvolvi um sistema multi-tier em que alguns dados eram provenientes de ficheiros XML, e que também era necessário escrever dados para ficheiros em XML. Na definição dos objectos de negócio da aplicação, além das propriedades estavam incluídos dois métodos, ToXML() e FromXML(XmlNode), que convertiam os dados do objecto para XML ou recebia os dados em XML e preenchia o objecto com esses dados.
Se tivesse apenas um ou dois objectos, não teria que me preocupar muito. O problema é que a aplicação era algo complexa e havia dezenas de objectos no sistema, cada um com o seu ToXML() e FromXML(). Havia características, especialmente a declaração do tipo do objecto e o mapeamento das propriedades que efectuei de forma particular para cada objecto. Com muitos objectos, o debug era uma chatice, as correcções eram constantes… enfim, admito que foi uma má escolha da minha parte.
De qualquer forma, no desenvolvimento de um novo site pessoal, onde os dados do site eram lidos de ficheiros XML, dediquei-me um pouco e criei uma classe que conseguisse resolver o meu problema e serializar e desserializar as minhas classes! Isto permitia a leitura directa do ficheiro XML para um objecto, ou no caso do backoffice escrever os dados do objecto directamente para o ficheiro XML.
Inicialmente, os métodos de conversão de XML para objecto e de objecto para XML estavam incluídos na classe que definia o objecto, com a personalização necessária a classe. Esta abordagem era evidentemente má, especialmente quando o numero de classes em que é grande pois não permitia a reutilização do código. Com tempo foi possível desenvolver uma classe generalizada, que permite a serialização e deserialização em XML dos objectos das diversas aplicações que integram a classe, aumentando a eficiência de desenvolvimento.
O artigo que segue, apresenta a estrutura típica em que é usado, a classe XmlCustomSerializer que efectua a serialização e desserialização do objecto automaticamente, e um exemplo de como utiliza-lo.
As aplicações que tenho desenvolvido, essencialmente na framework .NET (ASP.NET com C#), têm mantido a mesma estrutura multicamada. Aprendi este método e adaptei-o ás minhas necessidades após a leitura de um conjunto de artigos do Imar Spaanjaars (http://imar.spaanjaars.com – “Building Layered Web Aplications With Microsoft ASP.NET 2.0”).
Resumidamente, o método que ele descreve inclui uma camada de acesso a dados (DAL) uma camada de lógica de negócio (BLL) e a camada de apresentação. Comuns a estas 3 camadas estão os objectos de negócio (BO) que pouco mais são que contentores de objectos – classes definidas apenas com propriedades e, nalguns casos, alguns métodos que possam ser usados nas várias camadas. Podem ser incluídas mais ou menos camadas, que o conceito mantem-se - há a classe de suporte ás propriedades, global ás camadas, e um conjunto de classes gestoras, em cada camada.
A DAL tem tipicamente o conjunto de métodos CRUD de acesso à base de dados ou repositório de ficheiros. As classes da camada são gestores dos objectos no que diz respeito a acesso a dados. Por exemplo uma classe Pessoa, classe que é um BO com as propriedades de uma pessoa da aplicação, tem na DAL uma classe PessoaDAL, com o conjunto de métodos CRUD para gerir os dados de Pessoa na base de dados ou repositório. A classe tem os métodos Get(), GetList(), Insert(Pessoa), Update(Pessoa), Delete(Pessoa) e outro relevantes.
A BLL é composta também por classes gestoras dos objectos, mas com os métodos relevantes à camada de negócio – validações, autenticação, processamento de dados recebidos da aplicação ou da DAL, etc. Em alguns casos, os métodos da BLL são simples chamadas do método semelhante na DAL. A camada de apresentação tem o conjunto de métodos relevantes à apresentação da informação na aplicação e processamento das acções do utilizador.
Esta estrutura permite tornar transparente a forma como funcionam as diversas camadas. Apenas passa o conjunto de dados gerado pelas camadas para a seguinte, e por exemplo a camada de apresentação nunca terá acesso directo à camada de escrita de dados. Mais, é possível separar e distribuir as diversas camadas através de Remoting e/ou Web Services partilhando recursos entre aplicações ou distribuindo o esforço por diversas máquinas.
A serialização é um mecanismo importante na framework do .NET, em que os objectos são transformados numa stream de dados, formato portável, quer para utilizar remotamente, quer para persistir dados. No caso dos WebServices os dados das respostas são convertidos para XML, e o objecto a partir desses dados pode ser reconstruído. A serialização e desserialização neste caso é realizado automaticamente pela framework.
Na framework há três modos de serialização:
Quer a serialização binária quer a SOAP efectuam Deep Serialization no sentido que serializam o objecto na sua totalidade, incluindo outros objectos contidos nele. Já o XML, porque apenas serializa os elementos públicos do objecto, e é feita da forma como nós indicamos, é chamada de Shallow Serialization.
O resto do artigo está ligado à serialização costumizada em XML. É bastante útil para passar dados que estão em ficheiros XML directamente para um objecto ou vice versa. Há aplicações em que uma base de dados é overkill, e meia dúzia de ficheiros XML resolvem para suportar os dados. Escrever código dedicado a cada objecto é também muitas vezes desnecessário (a menos que segue um formato muito próprio e os objectos em uso não admitem a conversão directa).
Para o demonstrar vou criar um pequeno exemplo. Este servirá para apresentar o modo de implementação dos objectos como contentores, a classe genérica de serialização e a sua aplicação.
Um bom exemplo é uma lista de contactos. Os objectos de negócio que vou utilizar são:
A classe Pessoa contém os dados de uma pessoa (ID, nome e contactos) e utiliza ContactoList para associar vários contactos ao mesmo registo de Pessoa. ContactoList não é mais que uma lista genérica do tipo Contacto que contem os dados do contacto individual (tipo de contacto e o valor). PessoaList é também uma lista genérica, mas do tipo Pessoa.
A aplicação exemplo não utiliza as camadas BLL e DAL (a simplicidade não exige). No entanto declarei-os para apresentar um exemplo de como funciona. Como é possível ver no código, a DAL tem o conjunto de métodos que permite aceder à fonte de dados e retornar os objectos. Apenas esta camada tem acesso aos dados, o que torna o processo de obtenção de dados transparente para as camadas superiores. A BLL muitas vezes é uma simples interface para os métodos da DAL, podendo ter mais processamento, como validação e autenticação.
A camada de apresentação é constituída pelos vários formulários que a aplicação pode ter. Estes formulários apenas conhecem os métodos públicos da BLL que por sua vez apenas conhece os métodos públicos da DAL. A separação é implementada através de namespaces, e cada camada conhece apenas a camada seguinte. Este modo implementa uma clara separação da funcionalidade de cada camada. Assim, a camada superior, de apresentação tem acesso directo aos dados (não tem acesso directo à DAL). Qualquer tentativa de obter dados para apresentação tem de ser efectuado através de métodos disponíveis na BLL, através da inclusão do namespace usado para a BLL.
Relativo à aplicação, a funcionalidade implementada é o mínimo necessário para demonstrar o serializador. Primeiro, tem um campo que permite criar uma instância do objecto Pessoa através do nome. O ID é atribuído automaticamente pelo processo de inserção na lista PessoaList. À última pessoa criada é possível associar uma lista de contactos adicionando um contacto de cada vez. A caixa de texto apresenta o XML gerado pelo serializador a partir da instância do objecto PessoaList que suporta os dados. Por fim, é possível converter os dados do XML em numa nova instância de PessoaList.
A classe Form1 do formulário tem um membro declarado do tipo PessoaList para suportar a lista de pessoas:
public partial class Form1 : Form { PessoaList aLista = new PessoaList();
A adição de uma pessoa cria um objecto pessoa, com o nome preenchido e adiciona-o à lista de pessoas:
private void button1_Click(object sender, EventArgs e) { Pessoa p = new Pessoa(textBox1.Text); aLista.Add(p); //o ID neste exemplo simples passa a ser a posição na lista aLista[aLista.Count-1].ID = aLista.Count; //Com o construtor certo, as três linhas de cima poderiam ser escritas numa só, do genéro: // aLista.Add(new Pessoa(aLista.Count, textBox1.Text)); //com ID inicial 0 //SaveList - aqui surgiria a chamada ao processo para armazenar os dados. ShowXML(); }
A criação do ID é simulado neste caso – o ID após a inserção na lista é criado a partir do índice do registo na lista. O processo de armazenar os dados também é simulado, apenas mostrando o XML gerado na caixa de texto (o ShowXML será descrito mais á frente.)
Para adicionar contactos temos:
private void button2_Click(object sender, EventArgs e) { Contacto c = new Contacto(textBox2.Text,textBox4.Text); if (aLista[aLista.Count - 1].Contactos == null) aLista[aLista.Count - 1].Contactos = new ContactoList(); aLista[aLista.Count - 1].Contactos.Add(c); ShowXML(); }
É criado um novo contacto com o par valor / tipo no construtor. Se a lista de contactos da pessoa é nulo, é criado nova instância da lista. Finalmente é adicionado o contacto à lista e armazenada a lista.
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 publicos e privados. O attributo essencial para o processo de serialização é o que está declarado imediatamente antes do membro publico. [XmlElement] antes de Valor, indica que valor deve ser serializado como sendo um elemento XML; [XmlAttribute] antes de Tipo indica que Tipo será um attributo 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 utlização do atributo XMLType com a propriedade TypeName sobre a classe permite alterar o nome com que é serializado. O mesmo é possivel com o XmlElement através do parametro 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> { }
Resta substituir os campos a verde com os nomes desejados. O IDE corrige os get e set automaticamente. Apenas nos casos de usar tipos anulá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; } }
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.
A classe XmlCustSerializer é que efectua a conversão de e para Xml. Esta incluído no namespace dos BO para poder ser usado 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 memeoria 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 attributos 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 encondig UTF-8 UTF8Encoding encoding = new UTF8Encoding(); try { //Instancia uma Stream de memória, para onde é passo o texto Xml do parametro 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 parametro), transfere-o para a stream de memória, desserializa-o para o 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 instancia 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 datasource 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.
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 reconstruido como objecto PessoaList, em que as propriedades ID e Contactos da única Pessoa na lista são nulos. Esta caracteristica pode ser muito util 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.
Penso que é evidente a utilidade da classe XMLCustSerializer.
O uso do XML tem custos, no entanto:
Eu pessoalmente vou utilizado 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.
A classe XmlCustSerializer foi desenvolvido em conjunto com Marco Fernandes e Joaquim Rendeiro.
Algumas fontes usadas para compor a parte teórica do artigo: