Testes Unitários
Este artigo trata de conceitos que estão
relacionados direta ou indiretamente com
testes unitários. Veremos a definição, como
funcionam e como criar testes unitários para
seus projetos.
De que se trata o artigo
Este artigo trata de conceitos que estão relacionados direta ou indiretamente
com testes unitários. Veremos a definição, como funcionam e como criar
testes unitários para seus projetos. Além disso, abordaremos os mitos comuns
e mostraremos na prática como criar seus primeiros testes com o framework
NUnit.
Em que situação o tema é útil
Este tema é útil na garantia da qualidade dos projetos de software. Com testes
unitários bem definidos e implementados, você consegue identificar e corrigir
erros em tempo de desenvolvimento, você aumenta a confiança da equipe e do
cliente no projeto, consegue-se aumentar a produtividade da equipe, além de
identificar quais os pontos de seu sistema que possuem maior risco de
incidência de exceções, através da análise da cobertura dos testes.
Testes Unitários – Primeiros Passos
Neste artigo começaremos com a parte conceitual, explicando o que são testes
unitários, onde podem ser aplicados, além de discutirmos alguns mitos que
cercam este assunto, por exemplo, a questão da produtividade e custo de
implementação de testes unitários. Vencida esta fase partiremos para um
esclarecimento sobre as diferenças entre testes unitários e TDD. Daí então
entraremos na parte mais prática com a criação de exemplos de testes unitários
usando o NUnit.
De acordo com o Aurélio teste significa, dentre outras coisas, “Prova,
verificação da eficiência ou bom funcionamento (de máquinas, materiais
etc.)”.
Em toda indústria a questão dos testes é de suma importância para a qualidade
final dos produtos desenvolvidos. Usemos a indústria automobilística como
exemplo.
• O veículo passa por um “crash test” para avaliar o acionamento do air bag.
• O veículo passa por um teste de velocidade para avaliar o funcionamento dos
componentes internos do motor.
• O veículo passa por um teste no túnel de vento para avaliar a desempenho
aerodinâmico do mesmo.
Pois bem, a indústria de software, assim como as demais, possui diversas
técnicas e metodologias empregadas para garantir a qualidade e o bom
funcionamento do mesmo. Por exemplo, testes funcionais, testes de carga etc.
Enfim, existe uma infinidade de tipos e técnicas de testes de software, cada
uma com o seu propósito. Alguns destes testes devem ser executados por
“testes”, outros por usuários comuns e outros pelo próprio desenvolvedor,
como é o caso do teste unitário, que veremos neste artigo.
Testes Unitários
O teste unitário é o teste de menor nível no desenvolvimento de software, ou
seja, com ele testamos as menores estruturas que temos em nossos softwares,
que são os métodos.
Sendo assim, quando estamos falando de testar unitariamente o software,
estamos falando em criar métodos para testar os comportamentos de nosso
sistema. Por exemplo, se temos um método somar, teríamos o método de
testes “somarTest”.
Quando escrevemos um teste unitário para um método, desejamos garantir
que o comportamento do mesmo seja exatamente como o esperado nas mais
variadas possibilidades. A ideia é pegar os bugs logo que eles nasçam. Além
disso, o teste unitário se torna um ativo do projeto, ou seja, pode ser
reutilizado e executado diversas vezes sem onerar o seu tempo.
Testes unitários e TDD são coisas
diferentes
Um equívoco muito comum que vejo as pessoas cometerem no início de seus
estudos sobre testes unitários é a confusão com TDD. No início muita gente
acha que escrevendo testes unitários está desenvolvendo orientado a testes.
TDD é mais do que uma técnica, é um processo de desenvolvimento onde
todo código novo da aplicação é escrito a partir de um teste falho, ou seja,
você primeiro escreve um teste sem que exista o recurso, este teste
obviamente falhará, somente então você escreve a implementação do recurso.
Este processo possui três passos simples, conhecidos como Red, Green,
Refactor, onde:
• Red: você escreve o teste sem existir o código. Logo ele falhará.
• Green: implementa a solução, escrevendo o código necessário para que o
teste passe.
• Refactor: refatora a solução dada no item dois, de acordo com as boas
práticas de programação.
Nota do Devman
Refatoração é uma boa prática de desenvolvimento que consiste em uma série
de técnicas para melhorar a qualidade e legibilidade do código sem afetar o co
mportamento conhecido do sistema.
Pois bem, é preciso entender que testes unitários e TDD estão intimamente
relacionados, porém são coisas bem diferentes. O fato de você usar testes
unitários não significa que você use TDD.
Uma das diferenças chaves, é a ótica do tempo em que são realizados os
testes. Se você implementa seu código e depois cria seus testes, você
simplesmente usa testes unitários. Agora, a partir do momento em que você
começa a escrever seus testes antes mesmos de implementar a solução que
eles testarão, aí você está mais próximo do TDD.
Porém, o ponto fundamental é a motivação das duas técnicas. Enquanto testes
unitários têm como objetivo principal garantir que cada parte do sistema tenha
exatamente o comportamento esperado, o TDD tem como motivação principal
o design do código. Um dos grandes benefícios do uso do TDD é o design da
solução. Quando se pensa na ótica do consumidor do código antes mesmo do
código existir, você se obriga a pensar e avaliar as melhores e mais simples
formas de implementação da solução, o que traz um grande benefício para o
design da solução.
Nota do Devman
Quando nos referimos a design da solução, não estamos nos referindo à parte
estética, mas sim na parte arquitetural, na forma como os elementos do projeto
, os objetos, se comportam e colaboram entre si.
Mitos dos testes unitários
Assim como diversas outras técnicas, os testes unitários são alvos de críticas
que na verdade não passam de mitos. Alguns destes mitos são:
• Implementar testes unitários é muito caro: Esta opinião normalmente
vem de quem pensa que será necessário ter uma equipe de desenvolvedores de
testes unitários, que farão praticamente outro projeto só de testes ou da visão
de que precisa-se de ferramentas muito caras para fazer este tipo de testes e
que automatizar os mesmos custa uma fortuna. Na prática não é isso que
acontece quem escreve os testes unitários são os próprios desenvolvedores que
desenvolvem a solução e existem diversas frameworks free e open-source para
a criação e automatização de testes unitários.
• Testes unitários impactam o cronograma do projeto: Isto só é verdade
quando a equipe está no início da adoção dos testes unitários, porém isso não
é um privilégio, qualquer técnica ou prática que a equipe ainda não detenha
conhecimento, fará com que o tempo seja estendido, porém essa é uma fase
temporária, quando a equipe já tem uma certa maturidade no desenvolvimento
de testes, estes não impactam no cronograma do projeto.
• A equipe fica mais improdutiva, pois tem uma tarefa adicional a
fazer:Bom, para quebrar este mito vamos analisar a rotina tradicional de um
desenvolvedor para criar, por exemplo, uma tela de cadastro. Criam-se as
classes do modelo, implementa-se as regras de negócio, criam-se as classes de
persistência, daí, cria-se a interface com o usuário e somente então testa-se
tudo o que foi desenvolvido. Inclusão, alteração, exclusão, validações de
campos, regras de negócio etc. Agora vamos analisar este processo de testes
manuais do desenvolvedor, rodamos a aplicação, acessamos o recurso,
preenchemos o formulário e então clicamos em gravar para avaliar se o
comportamento foi como esperado. É comum as coisas não funcionarem de
primeira, então percebemos que falta um ajuste. Vamos ao código, acertamos,
executamos de novo a aplicação, acessamos o recurso, preenchemos o
formulário etc. Já parou para pensar no tempo que se gasta com este processo
“Executa, simula, verifica, corrige...”, quantas vezes ele é executado no seu
dia a dia? E se amanhã for feita uma alteração no requisito, você repetirá isso
mais N vezes. Sem contar que ao longo do desenvolvimento você acaba se
perdendo nos cenários em que já testou, faltando sempre testar um cenário
específico. Vejamos com testes unitários. Você cria o modelo e suas regras de
negócio, cria os testes para eles, executa. Se passar, parte para o próximo
passo e programa a persistência e seus respectivos testes e assim
sucessivamente. Quando você for realizar um teste do recurso todo, as
pequenas partes já estarão validadas e os ajustes serão muito menores a se
fazer. Se amanhã alterarem um requisito, você altera o sistema e roda os testes
(que já estão escritos). Revisa apenas os testes necessários sem ter que refazer
toda a bateria de testes do cenário anterior. Outro ponto que não percebemos,
mas que nos toma muito tempo e, com os testes unitários diminuímos
consideravelmente, é a realização de Debug. Colocando isso tudo na ponta do
lápis, você verá que a produtividade final da equipe tende a aumentar com a
prática de testes unitários.
• Tenho testes unitários, tenho qualidade e não preciso de mais
nada:Vamos lá, não existe bala de prata em desenvolvimento de software. Na
verdade existe, a verdadeira bala de prata no desenvolvimento de software é o
bom censo. Em testes, vale a mesma regra. Testes unitários têm seu escopo
limitado, eles não garantem a qualidade total do sistema, mas ajudam a pegar
e prevenir bugs e garantir que as pequenas partes do sistema façam
exatamente aquilo que se espera. Um problema que pode ocorrer, é que com o
teste unitário você garante que cada parte faz seu trabalho como deveria ser
feito, mas isso não lhe garante que quando você juntar todas as partes não
haverá algum tipo de conflito não previstos. Além disso, requisitos como
usabilidade e acessibilidade não podem ser validados com testes unitários.
Sendo assim, o uso dos mesmos não descarta a necessidade de outros testes
como funcional, aceitação, integração, dentre outros.
Nota do DevMan
Frameworks são bibliotecas de classes altamente reutilizáveis, que juntas, cola
boram entre si para atender uma demanda específica de parte de um software.
Por exemplo, Frameworks de persistência e Frameworks de Inversão de Contr
ole.
Como funcionam os testes unitários
Conforme citamos anteriormente, o teste unitário se aplica a menor unidade
comportamental que você tem em seus projetos, os métodos. Quando estamos
realizando um teste unitário, queremos garantir que um determinado método
tenha exatamente o comportamento que esperamos dele, de maneira geral os
testes consistem em criar objetos, alimentá-los com determinados valores,
executar determinados métodos e verificar se o resultado é igual ao valor
esperado.
Imagine um método somar da classe OperacaoMatematica. Suponhamos que o
método somar receba dois parâmetros, valor1 e valor2 e retorne o resultado da
soma. Em nosso teste, instanciaríamos um objeto da classe
OperacaoMatematica e em seguida chamaríamos o método somar deste,
passando dois valores quaisquer, por exemplo “5” e “3”. Após chamar o
método, verificaríamos se o retorno foi “8”. Caso tenha sido, consideramos
que o teste unitário passou, caso contrário assumimos que ele falhou.
Criando aplicação de exemplo
A prática de testes unitários é independe de tecnologia, portanto, não faz
diferença se seu projeto é em Windows Forms, Web ou WCF. Para fins
didáticos usaremos apenas uma aplicação Windows Forms.
Em nosso exemplo criaremos um cenário onde teremos três classes de
domínio, que serão Cliente, Pedido e ItemPedido. Além disso, teremos um
tipo enumerado para representar a categoria de cada cliente. Teremos ainda
uma regra simples de desconto por categoria de cliente.
Para criar nosso projeto de exemplo, abra o Visual Studio, opção File => New
Project => Windows Forms Application, conforme mostra a Figura 1.
Figura 1. Criação do novo projeto
É sempre uma boa prática separarmos nossas classes de negócio em um
assembly diferente do assembly principal da aplicação, com isso conseguimos
ter um desacoplamento maior e assim maior organização e reuso do código.
Seguindo esta boa prática, criaremos outro assembly que terá nossas classes
de negócio. Para criá-lo, clique com o botão direito sobre a solution, acesse na
opção Add => New Project e selecione a opção class library e nomeie a
mesma como [Link].
Outra boa prática é a separação dos namespaces de acordo com conceito
lógico das classes. Em nosso exemplo, teremos três conceitos bem distintos
para nosso assembly model, sendo eles, Business, Exceptions e Repository.
Por este motivo, vamos criar estas três pastas no nosso assembly
[Link]. Para criar estas pastas, clique com o botão direito sobre
o projeto [Link] e acesse Add => New Folder.
Vamos começar criando nosso tipo enumerado CategoriaCliente, que servirá
para indicar qual o desconto que o cliente terá, conforme mostrado
na Listagem 1.
Para criar o tipo enumerado, clique com o botão direito do mouse na pasta
Business, dentro do projeto [Link] e acesse a opção Add =>
New Item => Code File.
Um tipo enumerado é uma lista de valores constantes que tem uma
representação interna em um número. É sempre bom criarmos tipos
enumerados para melhorar a legibilidade do código. Esta prática torna a
aplicação mais fortemente tipada e consequentemente mais fácil de fazer
manutenção.
Listagem 1. Enumerado CategoriaCliente
namespace [Link]
{
public enum CategoriaCliente
{
Bronze,
Silver,
Gold
}
}
Após a criação da CategoriaCliente, vamos criar a classe cliente que terá um
atributo do tipo CategoriaCliente, conforme Listagem 2.
Listagem 2. Classe Cliente
using System;
using [Link];
using [Link];
using [Link];
namespace [Link]
{
public class Cliente
{
public string Nome { get; set; }
public DateTime DataNascimento { get; set; }
public CategoriaCliente Categoria { get; set; }
}
}
Criada a classe Cliente, vamos implementar agora a classe ItemPedido. Vale
ressaltar que temos uma composição entre Pedido e ItemPedido. Isso significa
dizer que objetos ItemPedido isolados não fazem sentidos se não tiverem
associados a um objeto Pedido.
Na classe ItemPedido, teremos o método CalcularSubTotal, que será alvo de
um teste unitário mais adiante em nosso artigo.
Para implementar esta composição, criaremos a classe ItemPedido que terá
como parâmetro de seu construtor um Pedido. Dessa forma, impedimos que
seja criado um ItemPedido sem pai. Podemos constatar isso na Listagem 3.
Nossa classe Pedido, terá um cliente associado e uma lista de ItemPedido.
Além disso, teremos um método para calcular o total do pedido já aplicando o
desconto de acordo com a categoria cliente do pedido. Para garantir a nossa
composição, criaremos ainda dois métodos para interagir com a lista de itens
privada, sendo o AddItem e o RemoverItemPedido, conforme podemos notar
na Listagem 4.
Listagem 3. Classe ItemPedido
using System;
using [Link];
using [Link];
using [Link];
namespace [Link]
{
public class ItemPedido
{
public Pedido Pedido { get; set; }
public ItemPedido(Pedido pedido)
{
[Link] = pedido;
}
public int Codigo { get; set; }
public string Descricao { get; set; }
public double Quantidade { get; set; }
public double ValorUnitario { get; set; }
public double CalcularSubTotal()
{
return Quantidade * ValorUnitario;
}
}
}
Listagem 4. Classe Pedido
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
namespace [Link]
{
public class Pedido
{
public Pedido()
{
Itens = new List<ItemPedido>();
}
private List<ItemPedido> Itens;
public Cliente Cliente { get; set; }
public ItemPedido AdicionarItem(int codigo, string
descricao, double quantidade, double valorUnitario)
{
ItemPedido item = new ItemPedido(this);
[Link] = codigo;
[Link] = descricao;
[Link] = quantidade;
[Link] = valorUnitario;
[Link](item);
return item;
}
public bool VerificaItemExisteNoPedido(int codigo)
{
ItemPedido itemLocalizado =
[Link](delegate(ItemPedido item) { return [Link] == codigo;
});
return itemLocalizado != null;
}
public void RemoverItem(int codigo)
{
ItemPedido itemLocalizado =
[Link](delegate(ItemPedido item) { return [Link] == codigo;
});
if (itemLocalizado != null)
[Link](itemLocalizado);
}
public double CalcularValorTotalPedido()
{
if(Cliente == null)
throw new ClienteNaoInformadoException();
double total = 0;
foreach(ItemPedido item in Itens){
total = total + [Link]();
}
double pecentualDesconto =
[Link](Clien
[Link]);
return total - (total * pecentualDesconto / 100);
}
}
}
Observe que no método CalcularValorTotalPedido da Listagem 4,
verificamos se o pedido possui um cliente e caso não possua, geramos uma
exceção de negócio. É uma boa prática usar exceções específicas para
comunicar violações de regras de negócio entre as camadas da aplicação.
Neste caso, a camada que chamar o método CalcularValorTotalPedido de um
pedido que ainda não esteja vinculado a um cliente, receberá uma exceção
ClienteNaoInformadoException. Com esta prática fica mais fácil tratar
exceções nas camadas consumidoras.
Vale ressaltar que throw é uma palavra reservada do .NET para lançamento de
exceções. A exceção ClienteNaoInformadoException é uma exception do
nosso projeto, que criamos na pasta Exceptions e herdamos da Exception base
do .NET, conforme mostrado na Listagem 5.
Listagem 5. Classe ClienteNaoInformadoException
using System;
using [Link];
using [Link];
using [Link];
namespace [Link]
{
public class ClienteNaoInformadoException : Exception
{
public ClienteNaoInformadoException()
{
}
public override string Message
{
get
{
return "O cliente não foi informado.";
}
}
}
}
Ainda observando o método CalcularValorTotalPedido, observe que
instanciamos um objeto do tipo ConfiguracaoDescontoRepository. Este objeto
seria uma implementação do pattern Repository.
O pattern Repository fornece uma classe que faz a mediação entre o domínio e
as classes de mapeamento e acesso a dados. No nosso exemplo, temos uma
lista privada em memória dentro de nosso repository, porém, em um projeto
real, poderíamos ter nosso repository acessando classes mais relacionadas à
infraestrutura da aplicação, por exemplo, DAO’s e classes de frameworks
ORM.
Nota do DevMan
DAO (Data Access Object) é um design pattern que prevê a separação de todo
o código de acesso a dados em um objeto específico para esta finalidade. Por e
xemplo, se você tem um cadastro de clientes, você teria a classe Cliente e a cl
asse ClienteDAO, onde ClienteDAO receberia um Cliente como parâmetro e s
eria responsável por persistir o mesmo no banco de dados. Quando o pattern D
AO é usado, todo o código SQL referente a uma determinada entidade deve fi
car na classe DAO da mesma. Neste caso todo Select, Insert, Update e Delete
da tabela Cliente, teria que ficar na classe ClienteDAO.
Isto torna o seu modelo mais independente desta camada de infraestrutura de p
ersistência além de tornar seu código mais reutilizável.
Frameworks ORM são bibliotecas de Mapeamento Objeto Relacional que tem
como objetivo fornecer uma camada de abstração entre o banco de dados (mo
delo relacional) e a aplicação (modelo de objetos). Entre os frameworks mais f
amosos temos o Entity Framework e o NHibernate. Normalmente estes frame
works fornecem meios de realizar o mapeamento de classes da aplicação para
tabelas do BD e de campos dos objetos para colunas das tabelas. Com isso di
minui-se o uso de SQL na aplicação, tornando o trabalho do desenvolvedor m
uito mais produtivo e a aplicação mais independente da base de dados.
Na Listagem 6 temos a implementação da classe
ConfiguracaoDescontoRepository, onde teremos uma lista em memória com o
percentual de desconto para cada categoria de cliente e forneceremos métodos
de acesso a esta lista. Vale ressaltar que estaremos usando os métodos
estáticos e a lista estática apenas para fins didáticos.
Listagem 6. Classe ConfiguracaoDescontoRepository
using System;
using [Link];
using [Link];
using [Link];
using [Link];
namespace [Link]
{
public static class ConfiguracaoDescontoRepository
{
private static Dictionary<CategoriaCliente, double>
listaPercentuaisDesconto = new Dictionary<CategoriaCliente, double>();
public static void AddDescontoCategoria(CategoriaCliente
categoria, double percentualDesconto)
{
[Link](categoria,
percentualDesconto);
}
public static double
GetPercentualDescontoPorCategoria(CategoriaCliente categoria)
{
return listaPercentuaisDesconto[categoria];
}
public static void ClearDescontosCategoria(){
[Link]();
}
}
}
Nota do DevMan
A palavra reservada static na classe indica que a classe não pode ser instancia
da e que todos os seus métodos precisam ser estáticos. Métodos e properties es
táticas são membros da classe e não da instância do objeto.
Por exemplo, em uma implementação comum, suponhamos uma classe Calcul
o com o método Somar. Para usar o método somar, precisamos criar um objet
o do tipo Calculo e então invocar o método somar deste objeto, por exemplo,
new Calculo().somar();
Caso o método somar fosse estático, nós não conseguiríamos acionar o mesmo
a partir do objeto, mas somente a partir da classe, sem necessidade de instanci
ar o objeto Calculo. Executaríamos apenas [Link]();
Por outro lado, a definição de properties estáticas na classe faz com que a mes
ma tenha comportamento semelhante, porém com um detalhe que é important
e observar, a definição do valor desta variável será compartilhada por toda a a
plicação. Portanto, é preciso tomar cuidado quando for usar variáveis e métod
os estáticos. Em nosso exemplo usaremos apenas para fins didáticos.
Preparando o projeto de testes
Com o nosso projeto e nossa library de domínio criada, vamos à criação de
nossas classes de testes. Seguindo nossa linha de desacoplamento, vamos criar
um novo Class Library chamado [Link]. Nele vamos criar duas
pastas, Business e Repository, que receberão nossas classes de testes.
A ideia é testar o comportamento público das classes, ou seja, a primeira coisa
que devemos fazer ao escrever nossos testes é desconsiderar métodos
privados. Analisando nosso modelo, teremos que criar três classes de testes,
para as classes ItemPedido, Pedido e ConfiguracaoDescontoRepository.
Em nosso exemplo usaremos o NUnit que é um framework de testes unitários
para .NET. Existem algumas opções de frameworks de testes no mercado,
inclusive da própria Microsoft, mas considero que será interessante começar
pelo NUnit para que se tenha exata noção da separação das responsabilidades,
até onde vai seus testes e até onde vai o trabalho do framework.
Podemos separar o NUnit em duas partes:
1. Framework: assembly contendo classes para criarmos nossos testes
unitários;
2. Front End: aplicativo para execução dos testes.
Baixe o NUnit, (seção Links), descompacte o mesmo em qualquer diretório.
Para criarmos nossos testes precisamos adicionar a referência do framework
ao nosso projeto, para isso clique com o botão direito sobre o projeto
[Link], acesse a opção Add Reference. Acesse a aba “Browse”,
navegue até o diretório onde foi extraído o NUnit, acesse a pasta Bin,
selecione o arquivo [Link] e clique em OK, conforme mostrado
na Figura 2.
Figura 2. Adicionando referência ao framework NUnit
Compreendendo os conceitos
chaves do NUnit
O NUnit se baseia em atributos para a definição das classes e métodos de teste
de nosso projeto. Toda classe de teste do NUnit precisa ser decorada com o
atributo [TestFixture]. Este atributo serve para indicar ao NUnit que aquela
classe é uma classe de testes do seu sistema. Além disso, todo método que
servirá para testar o comportamento de seu sistema precisa ser decorado com
o atributo [Test].
Com estes atributos nós conseguimos fazer com que nossas classes e métodos
de testes sejam enxergados pelo NUnit. Vale ressaltar que estes atributos estão
presentes no namespace [Link].
O outro ponto chave para criação de testes são as assertions. As mesmas são
as verificações que os métodos de testes irão fazer para garantir que o
comportamento do sistema é exatamente como o esperado.
O NUnit fornece uma série de métodos para assertions em nossos testes, por
exemplo:
• AreEqual: verifica-se dois valores, esperado e atual, são iguais.
• AreTrue: verifica se um determinado valor é verdadeiro.
• IsNotNull: verifica se determinada instância não é nula.
Basicamente o que devemos fazer quando estamos escrevendo nossos testes é
identificar os comportamentos públicos testáveis de nosso sistema, criar uma
classe para teste com o atributo TestFixture, criar os métodos com atributo
Test e implementar nossas verificações (assertions). Vejamos na prática como
se aplicam e se comportam estes conceitos.
Criando nossos testes
Vamos começar pela nossa classe mais simples, que é ItemPedido e possui o
método público CalcularSubTotal que retorna o resultado da multiplicação
entre a quantidade e o valor unitário do item.
Primeiramente vamos criar uma classe de testes para a classe ItemPedido,
conforme mostrado na Listagem 7.
Listagem 7. Classe TesteItemPedido
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
namespace [Link]
{
[TestFixture]
public class TesteItemPedido
{
[Test]
public void TestCalcularSubTotal()
{
Pedido pedido = new Pedido();
ItemPedido item =
[Link](1,"teste",20,100);
double subTotalCalculado = [Link]();
[Link](2000,subTotalCalculado);
}
}
}
Observe que o código da Listagem 7 é igual ao código de uma classe normal
de nossos sistema. No teste nós simulamos exatamente o consumo de uma
determinada classe e método. Neste caso, em nosso método
TestCalcularSubTotal, nós criamos um pedido vazio, adicionamos um item a
ele e guardamos a referência retornada pelo método AdicionarItem. Com esta
referência, nós chamamos o método calcularSubTotal da mesma e
armazenamos na variável subTotalCalculado. Após isso, faremos nossa
assertion através da classe estática do NUnit, Assert. Como nós inserimos um
item no pedido com quantidade 20 e valor 100, sabemos que o subtotal deve
ser 2000, então fazermos nossa verificação usando o AreEqual, passando o
valor esperado 2000 como primeiro parâmetro e o valor calculado pelo
método testado como segundo parâmetro. Caso os valores sejam iguais o
assertion retorna verdadeiro. O resultado deste assertion será o resultado de
nosso método TestCalcularSubTotal.
Feito isso, nosso primeiro teste está pronto. Para executarmos e visualizarmos
o resultado do mesmo vamos abrir o front end do NUnit, acessando o diretório
onde foi extraído o NUnit, pasta Bin, arquivo [Link], a interface inicial do
projeto será como a mostrada na Figura 3.
Figura 3. Tela inicial do front end do NUnit
Para executar nosso teste, precisamos carregar nosso projeto
[Link] no front-end do NUnit. Para isso, acesse o menu File =>
Open Project, acesse a pasta Bin do projeto [Link] e selecione o
arquivo [Link]. Após carregar o projeto, o NUnit irá carregar
todas as classes e métodos de testes do mesmo, conforme podemos ver
na Figura 4.
Figura 4. Projeto carregado no NUnit.
Na Figura 4 podemos observar que o nosso método de teste foi carregado na
árvore do lado esquerdo com uma imagem cinza. Esta imagem indica que o
teste não foi executado. Para executarmos os testes, basta clicar no botão Run.
O resultado será indicado visualmente com um sinal verde, para os testes que
passarem e um sinal vermelho para os testes que não passarem, conforme
mostrado na Figura 5.
Figura 5. Resultado da execução do teste.
Vamos continuar com a criação de nossas classes de testes. Após criarmos a
classe de teste para ItemPedido, vamos criar o teste de nosso repositório de
ConfiguracaoDesconto. Nesta classe podemos observar mais dois conceitos
fundamentais dos testes unitários, que são os conceitos de Setup e TearDown.
Em muitos dos testes que fazemos no nosso dia a dia, precisamos configurar
um determinado ambiente antes de iniciar os testes e depois restabelecer este
ambiente após a execução do teste. Um exemplo que gosto de citar é nos
testes envolvendo banco de dados. Você pode ter variáveis privadas em sua
classe de teste para armazenar a conexão com o banco de dados e configurar o
método de Setup para configurar a conexão e conectar ao BD e um método de
TearDown para fechar a conexão.
No NUnit estes conceitos são representados, respectivamente, pelos atributos
[SetUp] e [TearDown]. Devemos colocar estes atributos nos métodos que
serão responsáveis por configurar e zerar nosso ambiente de testes. O método
com o atributo [SetUp], será automaticamente executado antes da execução
dos testes, enquanto que o método com o atributo [TearDown] será
automaticamente executado após a execução dos seus testes.
Listagem 8. Classe TesteConfiguracaoDescontoRepository
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
namespace [Link]
{
[TestFixture]
public class TesteConfiguracaoDescontoRepository
{
[SetUp]
public void CarregarDescontos()
{
[Link](CategoriaCliente.B
ronze, 5);
}
[Test]
public void TestAddDescontoCategoria()
{
[Link](CategoriaCliente.G
old, 30);
double percentualDesconto =
[Link](Categ
[Link]);
[Link](30, percentualDesconto);
}
[Test]
public void TestGetPercentualDescontoPorCategoria()
{
double percentualDesconto =
[Link](Categ
[Link]);
[Link](5, percentualDesconto);
}
[TearDown]
public void LimparDescontos()
{
[Link]();
}
}
}
Como podemos ver na Listagem 8, temos o método CarregarDescontos como
Setup, que adiciona um desconto para a categoria bronze, para que esta
categoria sirva de insumo para a execução do teste
TestGetPercentualDescontoPorCategoria.
Além disso, temos o método LimparDescontos que zera toda a lista de
descontos que está em memória, garantindo que o ambiente final de testes seja
idêntico ao ambiente inicial.
No método TestAddDescontoCategoria, nós inserimos a configuração de
desconto para a categoria Gold e então recuperamos a mesma através do
GetPercentualDescontoPorCategoria e verificamos se o percentual de
desconto é o mesmo que acabamos de inserir.
Para finalizar nossos exemplos, fecharemos com a classe de testes para
Pedido. Nesta classe de testes, introduziremos mais um conceito importante na
realização de testes unitários que é a questão dos cenários de teste. É sempre
muito importante cobrirmos o máximo possível de condições em nossos
testes. É sempre bom testar cenários com valores válidos (mundo perfeito),
mas também com valores inválidos, passando parâmetros inválidos para os
métodos, forçando a geração de exceções, enfim, forçando o sistema a
executar seus fluxos alternativos, fora do caminho ideal. Dessa forma,
conseguimos avaliar se o comportamento do sistema está adequado quando os
fluxos alternativos são executados.
Listagem 9. Classe TestePedido
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
namespace [Link]
{
[TestFixture]
public class TestePedido
{
private const double VALOR_TOTAL_PEDIDO = 1350;
private Pedido pedido = null;
[SetUp]
public void CarregarPedido(){
Cliente cliente = new Cliente();
pedido = new Pedido();
[Link] = cliente;
[Link](1001,"Alicate de
Corte",50,10);//SubTotal 500
[Link](1002, "Chave de Fenda", 30,
25);//SubTotal 500
[Link](1003, "Chave Inglesa", 20,
5);//SubTotal 500
[Link](CategoriaCliente.B
ronze, 5);
[Link](CategoriaCliente.S
ilver, 10);
[Link](CategoriaCliente.G
old, 15);
}
[Test]
public void TestVerificaItemExisteNoPedido()
{
bool itemEncontrado =
[Link](1001);
[Link](itemEncontrado);
}
[Test]
public void TestVerificaItemNaoExisteNoPedido()
{
bool itemEncontrado =
[Link](2001);
[Link](itemEncontrado);
}
[Test]
public void TestAdicionarItem()
{
[Link](5001, "Parafuso", 500, 2);
bool itemEncontrado =
[Link](5001);
[Link](itemEncontrado);
}
[Test]
public void TestCalcularValorTotalPedidoSemCliente()
{
[Link] = null;
[Link](typeof(ClienteNaoInformadoException),
delegate() { [Link](); });
}
[Test]
public void
TestCalcularValorTotalPedidoClienteCategoriaBronze()
{
[Link] = [Link];
double percentualDescontoBronze =
[Link](Categ
[Link]);
double TotalPedidoCalculado =
[Link]();
double valorDescontoBronze = VALOR_TOTAL_PEDIDO *
percentualDescontoBronze / 100;
[Link](VALOR_TOTAL_PEDIDO -
valorDescontoBronze, TotalPedidoCalculado);
}
[Test]
public void
TestCalcularValorTotalPedidoClienteCategoriaSilver()
{
[Link] = [Link];
double percentualDescontoBronze =
[Link](Categ
[Link]);
double TotalPedidoCalculado =
[Link]();
double valorDescontoBronze = VALOR_TOTAL_PEDIDO *
percentualDescontoBronze / 100;
[Link](VALOR_TOTAL_PEDIDO -
valorDescontoBronze, TotalPedidoCalculado);
}
[Test]
public void
TestCalcularValorTotalPedidoClienteCategoriaGold()
{
[Link] = [Link];
double percentualDescontoBronze =
[Link](Categ
[Link]);
double TotalPedidoCalculado =
[Link]();
double valorDescontoBronze = VALOR_TOTAL_PEDIDO *
percentualDescontoBronze / 100;
[Link](VALOR_TOTAL_PEDIDO -
valorDescontoBronze, TotalPedidoCalculado);
}
[Test]
public void TestRemoverItem()
{
[Link](1001);
bool itemEncontrado =
[Link](1001);
[Link](itemEncontrado);
}
[TearDown]
public void LimparDescontos()
{
[Link]();
}
}
}
Observe na Listagem 9, temos o método CarregarPedido, que está
configurado como [SetUp], nele carregamos um objeto pedido com dados
fictícios, para que possamos usá-lo nos métodos de testes. Observe também
que temos uma constante, VALOR_TOTAL_PEDIDO, nela armazenamos o
valor total dos itens inseridos no pedido.
Observe que temos dois testes unitários para VerificaItemExisteNoPedido, um
buscando um item que está na lista do pedido, que adicionamos no método
CarregarPedido e outro passando um código de um item que não está na lista.
Dessa forma, testamos os dois cenários possíveis para este método ou seja,
encontrando e não encontrando o item.
Outro ponto importante a se destacar na questão dos cenários é o cálculo do
valor total do pedido. Na nossa classe de negócio, o cálculo leva em
consideração a categoria do cliente para então aplicar um desconto sobre o
valor total do pedido, sendo que caso o pedido não tenha um cliente,
disparamos uma exceção para interromper o fluxo.
Diante disso, trataremos os cenários possíveis. Temos três categorias fixas no
sistema e uma condição que trava a operação caso o pedido não tenha cliente
vinculado. Com isso, teremos quatro cenários diferentes para o
comportamento CalcularValorTotalPedido.
No método de teste TestCalcularValorTotalPedidoSemCliente, nós
simplesmente limpamos o cliente do pedido e tentamos calcular o valor total
do mesmo, porém observe que usamos o [Link]. Com este assert, o
NUnit executará o método passado via delegate no segundo parâmetro e
verificará se o mesmo lançará uma exceção do tipo passado no primeiro
parâmetro. Neste momento percebemos a importância de exceções bem
definidas em nosso sistema.
Nos demais métodos para calcular o total do pedido, nós alteramos a categoria
do cliente do pedido, recuperamos o percentual da categoria em questão,
calculamos o total e verificamos se bate com a nossa conta.
Por fim, testamos o método de remoção de item, passando um código que nós
sabemos que está na lista e depois verificando se o mesmo não consta mais no
pedido.
Ao gerarmos um novo build do nosso projeto de testes, abrirmos novamente o
NUnit e rodarmos o testes, temos o resultado demonstrado na Figura 6.
Figura 6. Execução de todos os testes
Para finalizar gostaria de salientar, que é muito importante seguir padrões para
seus testes. Namespaces, nomenclaturas, estruturação etc. O nome, por
exemplo, não importa se você vai usar um prefixo ou sufixo, o importante é
seguir um padrão, como usamos em nossos exemplos. Além disso, tenha com
seus testes, o mesmo cuidado e carinho que tem com seu código. Não é
porque o código de teste não vai para produção, que iremos abrir mão de
aplicar boas práticas, organização e clareza no mesmo.
Conclusão
Como vimos neste artigo, os testes unitários podem nos ajudar muito na
qualidade de nossos projetos. É importante frisar que o nível de detalhamento,
o nível de cobertura de testes, vai depender muito do projeto e da equipe. O
que nós vimos aqui foi apenas uma introdução, para que você possa seguir
adiante neste mundo a parte que é o de testes unitários.
Todo início de aprendizado é complicado, mas sempre temos que passar por
este início. Após implementar seus primeiros testes, você começará a ver o
mundo de oportunidades que os testes lhe trazem. Você poderá migrar para o
TDD mais facilmente, poderá automatizar seus testes, criar um ambiente de
integração continua em sua equipe e passar a medir a cobertura de seus testes,
isto é, medir qual a proporção de seu código está coberta por testes. Este
artigo é só o começo.
Links
Site do NUNIT
[Link]
Download do NUnit
[Link]
por Ricardo da Silva Coelho
Guru .net e tecnologias MS
Styles em WPF
O artigo trata do uso de estilos em
aplicações WPF. O mesmo é importante
quando se deseja criar aplicações
padronizadas.
Do que trata o artigo
O artigo trata do uso de estilos em aplicações WPF. O mesmo é importante
quando se deseja criar aplicações padronizadas. Na prática, em vez de alterar
arquivo por arquivo, utilizando os estilos será possível concentrar toda a
estrutura de definições do layout (como cores, fontes etc.) em um único
arquivo.
Para que serve
Os estilos são usados para alterar a aparência dos controles de uma aplicação
XAML e podem servir tanto para a personalização como para aperfeiçoar a
iteração do usuário com a aplicação.
Styles com WPF
O surgimento das aplicações WPF representou um aperfeiçoamento nas
interfaces com usuário para projetos desktop. Não somente por causa das
possibilidades existentes para alterar a aparência dos elementos da interface,
aplicando efeitos, cores, gradientes, imagens e animação, mas também porque
isto tudo pode ser feito de uma maneira organizada e com grande índice de
reaproveitamento do código.
Ao desenvolver os projetos WPF usando a linguagem de marcação XAML, os
programadores que estão começando, podem perceber alguma semelhança das
suas práticas e padrões com os que são usados no desenvolvimento de
aplicativos browser based.
Esta semelhança se dá principalmente porque o próprio XAML é um derivado
da linguagem XML e essa tem suas raízes no HTML. Além disto, o
desenvolvedor pode ter maior controle do comportamento da interface com o
XAML e de uma maneira mais simples desde que os seus conceitos sejam
bem conhecidos e aprofundados.
Os novos tipos de projetos disponibilizados no Visual Studio a partir da
versão 2008 como aplicações [Link] MVC e WPF tendem a trazer o
controle da aplicação de volta para o programador, o que no ponto de vista de
alguns desenvolvedores estava se perdendo com as IDE´s cada vez mais
automatizadas.
A interface com o usuário sempre foi um dos pontos mais críticos nas
aplicações desktop para o sistema operacional Windows. E este ficou durante
muito tempo devendo em recursos mais elegantes.
Entretanto, há alguns anos, o Framework .NET começou a quebrar esta
barreira de desenvolvimento de programas para o Desktop (e também para a
Internet) com os projetos WPF/Silverlight e a linguagem de marcação XAML
usada para definição dos elementos da interface e estilos.
O XAML (Extensible Application Markup Language) é a base para a
definição dos elementos de interface para as aplicações WPF, sendo utilizada
principalmente como forma de fazer uma representação dos objetos e
controles de uma forma compreensível. Ela é derivada da linguagem XML e
possui regras bem definidas. Isto permite um grande número de combinações
e composições, o que a torna perfeita para a tarefa de definição de interface.
Ainda é possível fazer o design da interface graficamente. Veja que na Figura
1 o Visual Studio 2010 demonstra uma janela sendo editada enquanto também
é visível uma área para ajustes no código XAML.
Figura 1. Design de janela em WPF mostrando a marcação XAML
A própria interface do Visual Studio induz você a ter contato com a marcação
por razões bem simples. Primeiro porque é bem estruturada. Além disso,
depois que você conhece os seus princípios você consegue controlar vários
aspectos, que vão desde a definição de estilos até rastreamento de eventos nos
elementos visuais. Também pode ser mais produtivo para alguns editar
diretamente o código em vez de usar o editor de propriedades. Isto porque em
alguns casos a forma como se definem atributos para os elementos da janela
percorre um caminho mais longo do que se for feito manualmente, editando o
código. Dependendo do que se deseja fazer é quase inviável se não for
editando a marcação diretamente. Considere o exemplo da Listagem 1.
Listagem 1. Exemplo de marcação XAML
27 <ListView x:Name="listaUsuario"
28 Style="{StaticResource listViewStyle}"
29 ItemContainerStyle="{StaticResource
ResourceKey=doubleClick}"
30 ItemsSource="{Binding}">
31 <[Link]>
32 <GridView>
33 <GridViewColumn>
34 <[Link]>
35 <DataTemplate>
36 <StackPanel
Orientation="Horizontal">
37 <Image Width="16"
Height="16"
38
Source="imagens\user_sticker_64.png"/>
39 </StackPanel>
40 </DataTemplate>
41 </[Link]>
42 </GridViewColumn>
A porção do código que vai das linhas 31 até 42 faz a definição de uma célula
que mostra uma figura em uma grade para representar registros linearmente.
Note que foram usados vários elementos de marcação aninhados e nisto reside
um dos maiores diferenciais da linguagem XAML. Enquanto nos projetos
Windows Forms a criação de um novo componente de imagem é bem difícil,
sendo necessário um profundo conhecimento de orientação a objetos e da
parte gráfica do desenvolvimento para o Windows e o .NET, nos projetos do
WPF você consegue agrupar controles diferentes para obter um visual
totalmente novo e muitas vezes consegue reutilizar este código de maneira
eficiente.
Se você já desenvolveu alguma aplicação usando o XAML deve conhecer os
elementos da listagem. As tags correspondem a containers, controles e
elementos para definição de layout. Os atributos das marcações são as
propriedades que precisam ser ajustadas. Com apenas isto você já consegue
começar a desenvolver com WPF e XAML. Uma boa dica para ajudar no
aprendizado desta linguagem é você ir trabalhando na aba designer e sempre ir
analisando o código que está sendo gerado na marcação XAML.
Estilos
Antes de falar sobre estilos é preciso entender a que estamos nos referindo, já
que este termo aponta para “N” significados. Quando me refiro a estilos estou
falando de ajustes nos controles visuais. Então é muito grande o número de
ajustes que podem ser feitos. Cito como os principais:
1. Cor usada nos elementos tanto de frente como de fundo.
2. Definições para fontes usadas nos componentes que apresentam texto.
Todas as propriedades conhecidas das fontes podem ser ajustadas.
3. Geometria dos elementos, alterando seu tamanho e até a sua forma em
alguns casos. No exemplo prático deste artigo usei esta alteração de uma
forma bem simples, que foi a alteração das bordas, deixando-as arredondadas.
4. Criação de efeitos de gradiente com as cores, possibilitando misturar duas
ou mais cores para obter efeitos que deixam a interface mais interessante.
5. Aparência do elemento visual quando determinados eventos ocorrem como
o ponteiro do mouse passar sobre algum controle ou houver um click e assim
por diante. Este tipo de comportamento é denominado Event Triggers porque
o efeito é “disparado” quando algum evento ocorre com o controle.
Como se pode esperar é possível que você ajuste a aparência diretamente no
seu componente. Assim, é totalmente aceitável um código como o seguinte:
<Label Content="{Binding Name}" VerticalAlignment="Center"
FontSize="12" Width="180" />
Aqui está sendo definido um elemento Label que é usado para mostrar algum
texto sem permitir a sua edição. Observe os atributos VerticalAlignment,
FontSize e Width que ajustam a aparência do controle.
Uma situação onde este tipo de marcação é gerado é quando a interface é
composta usando o design gráfico do Visual Studio, já que com o editor de
propriedades é muito prático fazer o ajuste das propriedades visuais.
Dependendo do tamanho do projeto e do prazo que existe para a sua entrega
esta é a forma mais adequada de trabalhar com a formatação. Porém,
conceitualmente não estamos trabalhando com estilos porque neste caso, cada
elemento é formatado individualmente e não há um padrão sendo seguido.
Assim, se precisamos mudar o padrão de formatação usado nos controles do
tipo TextBox por exemplo, vai ser necessário ir alterando um a um.
Logicamente isto não é uma boa ideia e vai atrasar muito a finalização do
projeto.
Folhas de estilos XAML
Por causa da necessidade de manter um padrão visual em toda a aplicação,
devemos considerar outra forma de ajustar e formatar os controles, que é a
utilização dos estilos definidos em um local centralizado como um arquivo
XAML com conteúdo exclusivo para este fim. Isto é feito no Visual Studio de
várias formas, sendo que a mais comum é a criação de um elemento designado
no projeto como ResourceDictionary.
A sua criação é bastante simples, usando os mesmos passos que são dados
para criar qualquer elemento do projeto como uma classe, uma janela etc.
Então, o mais importante é entender o comportamento deste elemento e
aspectos importantes do seu funcionamento.
Considere que o que vai dentro do arquivo é a marcação XAML então, o que
vai diferenciá-lo de uma janela? Exatamente o tipo da marcação colocada no
início do arquivo. Enquanto em uma janela todos os elementos estão contidos
entre as tags <Window></Window>, um ResourceDictionary utiliza as tags
<ResourceDictionary></ResourceDictionary>.
A estrutura básica para definir um estilo pode ser conferida a seguir:
<Style TargetType="{x:Type ListView}"
x:Key="listViewStyle">
<Setter Property="FontFamily"
Value="Courier New" />
</Style>
A tag <Style> indica o início da definição para um estilo. É sempre bom
lembrar que como a linguagem XAML deriva de XML as regras desta são
sempre válidas. Entre as mais importantes cito a necessidade de observar o
case das tags e também a necessidade de haver uma tag de fechamento para
cada uma aberta. Tendo isto em vista, o primeiro passo é identificar sobre qual
elemento da interface o estilo pode ser aplicado através do atributo
TargetType. Entre os valores que podem ser informados ali, se encontra o tipo
do componente visual, que pode ser qualquer um suportado pelo WPF.
Em seguida, o estilo precisa ter uma key o identificando para que
posteriormente seja aplicado ao elemento visual apropriado. O estilo definido
anteriormente é aplicado no controle visual desta forma:
<ListView x:Name="listaUsuario"
Style="{StaticResource listViewStyle}"
Observe a key definida sendo utilizada no atributo Style. Assim, de uma
maneira bem direta ocorre com os demais estilos que forem sendo definidos.
Continuando a entender como são definidos os estilos, dentro da tag <Style> é
definido um número variável de tags <Setter>. Estas configuram qual a
propriedade do controle que desejamos modificar no atributo Property e, no
atributo Value, informamos qual o valor a ser preenchido. Para a tag Property
é preciso conhecer o nome correto do elemento a ser ajustado. Isto pode ser
feito consultando os membros (propriedades) da classe vinculada com o
controle na documentação do Framework .NET. Da mesma forma, é preciso
observar quais são os valores aceitos, pois na marcação XAML os mesmos
são passados como uma string, mas, em tempo de execução os valores serão
convertidos para o tipo apropriado vinculado com a propriedade. Caso o valor
seja incorreto o programa não irá rodar.
Uma questão interessante a ser observada é a possibilidade de termos vários
estilos que são aplicados ao mesmo tipo de controle. Dependendo das
circunstâncias você pode querer usar formatos diferentes para o mesmo tipo
de controle, assim, a maneira de diferenciar estes estilos é usar chaves
apropriadas no atributo x:Key.
Há mais possibilidades ainda de estilos que podem ser definidos em um
ResourceDictionary. Entre os recursos interessantes que serão vistos na
aplicação que foi construída para demonstração, está à criação de gradientes
de cores para preencher controles. Estes gradientes estão entre os meus efeitos
preferidos, principalmente porque faze-los usando as aplicações Windows
Forms tradicionais eram realmente muito difícil.
É possível também que você deseje separar os estilos por afinidade, colocando
estilos relacionados com cada tipo de controle em um ResourceDictionary
apropriado. Isto não é problema, principalmente porque para que estes possam
ser usados na aplicação, precisam ser referenciados no arquivo [Link]
que é o ponto de entrada de uma aplicação WPF.
Embora neste artigo esteja usando ResourceDictionary para definir estilos, sua
aplicação vai muito além. Como o próprio nome indica, este arquivo serve
como um repositório de recursos para o projeto e pode conter estilos,
indicação de imagens que serão usadas no projeto, definição de modelos para
controles, permitindo alterar completamente os elementos do mesmo,
conforme o estilo aplicado. Além disso, é necessário fazer uma consideração
importante sobre os estilos.
Neste artigo estou fazendo a definição de estilos de forma estática. Eu crio o
estilo e faço o vínculo do mesmo com a interface em tempo de projeto. A
aplicação terá sempre a mesma aparência, porém, é possível criar vários
destes estilos em arquivos separados e, dinamicamente, aplicar a um controle
em tempo de execução usando a linguagem C# para vincular o estilo com o
controle. Este assunto será abordado futuramente em outro artigo e por hora
basta saber que é possível sim usar estilos dinâmicos em uma aplicação.
O projeto de exemplo
Para poder demonstrar os conceitos, elaborei um pequeno exemplo cujo
objetivo é aplicar os estilos em alguns dos principais tipos de controle que são
comuns nos projetos do WPF.
A aplicação possui uma janela principal, onde é mostrada uma lista de nomes,
que podem ser os usuários de um sistema, por exemplo.
A Figura 2 demonstra como esta janela vai ficar em sua versão final.
Figura 2. Janela principal
Nesta janela alguns elementos importantes serão vistos como bordas,
preenchimento do background. Note que o fundo do título do formulário usa
um efeito de gradiente, algo que é bem fácil de fazer usando os estilos em
WPF.
O outro controle chave desta janela é a ListView, que mostra os dados do
usuário e possibilita usar uma imagem. Durante o exemplo estou mostrando
como exibir uma imagem neste tipo de controle e também faço considerações
sobre como fazer estes ajustes.
Ao dar um duplo clique em um item da lista, uma janela como a da Figura 3 é
exibida com os detalhes do usuário selecionado.
Figura 3. Exibição de detalhes
Um ponto importante é que além de você poder ter contato com a utilização
dos estilos, poderá também tirar algumas dúvidas sobre como vincular os
controles visuais com os objetos de dados da sua aplicação.
Outro aspecto interessante é que este projeto foi criado promovendo a
separação de responsabilidades, ou seja, as classes para os dados estão em um
projeto do tipo class library que é referenciado no projeto responsável pela
interface com o usuário.
Criando a Solution
A criação de um projeto do tipo WPF no Visual Studio 2010 segue os
caminhos comuns aos demais. Na Figura 4 estou ilustrando como criar
partindo da janela de boas vindas (ou Start Page).
Figura 4. Criando uma solution vazia
Não há nenhuma diferença em seguir este caminho ou usar o menu File >
New Project. A única diferença é que dependendo das suas configurações,
esta página inicial pode não estar sendo exibida. Mas, ambos os
procedimentos abrem uma janela para que seja possível escolher detalhes do
projeto que está sendo criado. Esta janela está demonstrada na Figura 5.
Para este exemplo eu optei por iniciar com uma solution em branco.
Se você for criar o projeto, fique a vontade de já inserir um projeto para a
class library ou a interface, apenas observando que estes passos serão
demonstrados mais adiante.
Class Library é uma biblioteca de recursos criada a partir da plataforma .NET,
normalmente gerada como um arquivo com a extensão .dll. Este tipo de
estrutura encapsula elementos como classes, interfaces, enumerations,
eventos, controles visuais etc. Desta forma, permite que tais construções de
código possam ser reutilizadas ao longo de uma série de projetos.
Figura 5. Janela do assistente para criar um novo projeto
Para a solution de exemplo, foi utilizado o .NET Framework 4, mas é possível
criar o projeto também com a versão 3.5.
O projeto para os dados
A aplicação mostra alguns dados (informações) para poder também
demonstrar além dos estilos, como fazer a ligação entre os controles visuais e
os objetos de negócio da aplicação.
No caso deste exemplo eu criei um projeto separado dentro da solution para
permitir exercitar o conceito de separação de responsabilidades. Para
adicionar este projeto observe os passos da Figura 6 que ilustra o menu
exibido ao clicar com o botão direito do mouse sobre o ícone da solution.
Figura 6. Como adicionar um novo projeto
Esta ação abre uma janela como a da Figura 7 que oferece opções para a
criação do projeto.
Figura 7. Assistente para criar novo projeto
Para abrigar as classes usadas para os dados da minha aplicação optei pelo
projeto do tipo Class Library que gera ao ser compilado um arquivo DLL.
Este projeto será referenciado mais tarde, dentro do projeto de interface com o
usuário.
A versão do framework utilizado foi a quatro, também para manter o padrão e,
a linguagem o C#, como é de costume.
Denomei o projeto como [Link] apenas para poder ficar mais
simples de identificar que este projeto faz a modelagem dos dados, mas, isto
não é obrigatório. Quando se está criando projetos com esta finalidade, apenas
deixa um pouco mais fácil de identificar a função de cada projeto.
Com o projeto adicionado, o próximo passo é adicionar uma classe para
definir a estrutura do objeto Usuário. A Figura 8 demonstra os passos que são
necessários para executar esta tarefa.
Esta ação é feita ao se clicar sobre o ícone do projeto recém-criado com o
botão direito do mouse e escolher as opções Add > Class.
Figura 8. Adicionando uma classe
Novamente, esta ação dispara uma janela que pode ser conferida na Figura 9.
Tudo o que se deve fazer é nomear a classe.
Figura 9. Criando a classe usuário
Não vou fazer considerações sobre persistência dos dados, ou seja, como estes
serão armazenados para posterior recuperação. A classe, cuja listagem pode
ser conferida na Listagem 2 é bem simples.
Listagem 2. Classe Usuario
namespace [Link]
{
public class Usuario
{
public int ID { get; set; }
public string Nome { get; set; }
public string Login { get; set; }
public string Email { get; set; }
public bool Ativo { get; set; }
}
}
Note que apenas foram definidas as propriedades para a classe, já que
nenhuma ação especial será executada pela mesma.
Os campos do tipo string são bem simples de serem conectados com os
controles visuais, mas, um detalhe interessante é o comportamento da
propriedade Ativo quando for conectada com o controle CheckBox. Mais
adiante isto poderá ser conferido.
Estou mantendo a classe de dados bem simples para poder dar foco maior no
tema do artigo, que é a utilização de estilos.
Se consideremos que é necessário cuidar de como os dados devem ser
enviados para um banco de dados e como devem ser recuperados, acaba se
tomando espaço e perdendo o foco do artigo.
Assim, necessito de prover para a interface do usuário alguns dados de
exemplo. Para isso, optei por criar uma classe que retorna uma lista com
alguns usuários já preenchidos.
Esta classe foi criada usando os mesmos passos anteriores e pode ser
conferida na Listagem 3.
Listagem 3. Classe DataFactory
1 using [Link];
2
3 namespace [Link]
4 {
5 public static class DataFactory
6 {
7 public static List<Usuario> retornarListaDeUsuarios()
8 {
9 List<Usuario> usuarios = new List<Usuario>();
10 [Link](
11 new Usuario
12 {
13 Ativo = true,
14 Email = "joaosilva@[Link]",
15 ID = 1,
16 Login = "joaos",
17 Nome = "João da Silva"
18 });
19 [Link](
20 new Usuario
21 {
22 Ativo = true,
23 Email = "paulocardoso@[Link]",
24 ID = 2,
25 Login = "pauloc",
26 Nome = "Paulo Cardoso"
27 });
28 [Link](
29 new Usuario
30 {
31 Ativo = false,
32 Email = "patriciafernanda@[Link]",
33 ID = 3,
34 Login = "patriciaf",
35 Nome = "Patrícia Fernanda"
36 });
37 [Link](
38 new Usuario
39 {
40 Ativo = true,
41 Email = "giovanaaline@[Link]",
42 ID = 4,
43 Login = "giovanaa",
44 Nome = "GiovanaAline"
45 });
46 return usuarios;
47 }
48 }
49 }
Novamente a classe é bastante simples. Sua extensão (em número de linhas) é
porque muito código precisou ser repetido para gerar um número razoável de
dados para a aplicação.
Esta classe é do tipo static (linha 5) o que significa que não precisamos de um
objeto para acessar o método retornarListaDeUsuario e também que o seu
conteúdo será sempre o mesmo, não importando de onde a mesma esteja
sendo acessada.
Primeiramente é definida uma lista genérica List<T> na linha 9 para receber
objetos do tipo Usuario. Da linha 10 até a linha 45 estou adicionando novas
instâncias desta classe para a lista, usando um tipo de inicialização direta.
Sua compreensão é simples. O método Add da lista genérica de usuários
espera uma instância desta classe. Assim, como o construtor devolve um
objeto deste tipo, estou usando o construtor passando os valores para as
propriedades diretamente.
Este tipo de notação não interfere no resultado final, apenas resume bastante o
código. Não há problema em se criar um objeto, passar os valores para as
propriedades e só então enviar este para a lista. Se for executar o exemplo,
fique à vontade para codificar como achar mais apropriado.
Com as classes Usuário e DataFactory criadas, o projeto para modelagem dos
dados está concluído.
O projeto para a interface com o
usuário
Na solution foi adicionado um projeto do tipo WPF Application usando os
mesmos procedimentos anteriormente usados no projeto da modelagem dos
dados.
A Figura 10 mostra os detalhes da janela de diálogo que auxilia na criação.
Este projeto foi nomeado como [Link] para manter um padrão de
nomenclatura.
Figura 10. Assistente para criar o projeto
Ao criar este tipo de aplicação (WPF Application) o Visual Studio já cria toda
a estrutura necessária com os principais elementos que serão usados, dentre
eles, os arquivos XAML. Optei por manter este padrão e não fazer
modificações.
Vou separar a elaboração da interface em duas partes. Primeiramente, vou
apresentar o layout básico para as janelas, sem definir nenhum estilo.
Farei assim para que fique claro quais as mudanças são necessárias para
adicionar estilos nas aplicações. Isto é feito de maneira bem clara, como
poderá ser observado. Mas, antes é necessário conectar os projetos.
Conectando os projetos
Ao criar um projeto separado para modelagem dos dados e outro para a
definição da interface com o usuário (UI), mesmo que os dois estejam na
mesma solution do Visual Studio, será necessário criar uma referência para
que seja possível usar os dados definidos na modelagem dentro da interface.
Isto é feito de maneira muito prática, bastando que se clique com o botão
direito do mouse sobre o ícone do projeto da UI e escolha a opção Add
Reference.
Esta ação está ilustrada na Figura 11.
Figura 11. Acessando o menu para adicionar referência
Existem algumas opções na janela para adicionar uma referência para um
projeto. No caso do exemplo, estou interessado em adicionar uma referência
para um projeto existente dentro da mesma solution (Figura 12).
Figura 12. Assistente para adicionar referência para um projeto
A vantagem de se referenciar o projeto da mesma solution é que caso alguma
alteração tenha sido feita no projeto referenciado, enquanto a solution está
aberta, ao compilar o projeto da UI o Visual Studio verifica se é necessário e
também compila o projeto referenciado.
Com esta ação executada, já podemos partir para a definição do layout das
janelas. Como optei por deixar a estrutura do projeto definida pelo template
do Visual Studio, basta clicar no arquivo MainWindow para exibir o conteúdo
desta janela. A Listagem 4 mostra como deve ficar o código inicial desta
janela.
Listagem 4. Marcação XAML para a lista de usuários
1 <Window x:Class="[Link]"
2
xmlns="[Link]
3 xmlns:x="[Link]
4 Title="[Link] Janela principal"
5 Height="350"
6 Initialized="Init"
7 Width="525">
8 <[Link]>
9 <Style x:Key="doubleClick"
10 TargetType="{x:Type ListViewItem}">
11 <EventSetter Event="MouseDoubleClick"
12 Handler="listView_DoubleClick" />
13 </Style>
14 </[Link]>
15 <DockPanel>
16 <Border [Link]="Top"
17 x:Name="bordaPrincipal">
18 <TextBlock x:Name="tituloPrincipal">
19 Listagem de Usuários
20 </TextBlock>
21 </Border>
22 <Border [Link]="Top"
23 x:Name="bordaListagem">
24 <ListView x:Name="listaUsuario"
25 ItemContainerStyle="{StaticResource
ResourceKey=doubleClick}"
26 ItemsSource="{Binding}">
27 <[Link]>
28 <GridView>
29 <GridViewColumn Header="ID"
30 Width="100"
31
[Link]="Right"
32
DisplayMemberBinding="{Binding ID}"/>
33 <GridViewColumn Header="Nome"
34 Width="Auto"
35
DisplayMemberBinding="{Binding Nome}"/>
36 </GridView>
37 </[Link]>
38 </ListView>
39 </Border>
40 </DockPanel>
41 </Window>
Uma das grandes vantagens da linguagem XAML é a maneira rápida e direta
de se definir tantos elementos do layout, por exemplo, métodos respondendo a
determinados eventos que ocorrem com os controles visuais. Neste exemplo,
logo na linha 6, o evento Initialized (que é disparado logo que a janela é
criada) foi vinculado com um método chamado Init. Este método será descrito
quando estivermos tratando do código C# usado para preencher os dados da
janela.
Nesta janela, antes de definir os elementos de layout e controles, foi criado
uma marcação para ser usada em um controle ListView, que será usado para
apresentar a lista de usuários. Na linha 8 inicia-se uma tag
[Link]. Estes podem conter desde marcações para definir estilos
a serem usados como, no caso deste exemplo, configurar um evento para um
controle. Na linha 9 foi configurado uma chave para um evento com um
controle do tipo ListView (linha 10). Com a x:Key que foi definida –
doubleClick, depois podemos configurar o método que foi passado na tag
EventSetter na propriedade Handler (e que será descrita quando formos tratar
do code behind desta janela) na linha 11 para responder a um duplo clique do
mouse sobre a ListView. O evento que deve disparar o método está definido
na propriedade Event, na linha 11.
O primeiro ponto a observar na listagem em relação aos controles, é que todos
estão localizados em um DockPanel, como pode ser notado na linha 15. Ao
fazer isto e configurar a propriedade [Link] dos controles, (como
veremos mais a frente) o layout da janela fica correto, mesmo ao
redimensionar esta.
As outras opções mais comuns para o layout dos controles são o Grid e o
StackPanel. É importante observar que mesmo usando um DockPanel como
principal container para controles na janela, a linguagem XAML e o WPF
permite que tenhamos outros containers aninhados.
O primeiro controle ou elemento de layout que é configurado é um Border
(linha 16) esta será usada para compor o título da janela e está ancorada no
topo da janela, através da propriedade [Link]. Embora não seja
necessário, este elemento recebeu um nome com a propriedade x:Name.
Para exibir o texto que serve como título para o conteúdo da janela, foi usado
um TextBlock (linha 18), embora também pudéssemos usar um Label.
Na linha 22 foi definida outra borda para servir de contorno para o controle
ListView que é definido na linha 24.
Com relação ao controle ListView, a propriedade ItemContainerStyle vincula
o controle com o tratamento do evento de duplo clique do mouse (linha 25)
que foi definido no topo da listagem.
A propriedade ItemsSource define a fonte para os dados que vão popular este
controle. Ao usar {Binding} estamos configurando o controle para usar a
coleção de objetos que será definida no código e buscar por propriedades que
serão passadas mais adiante.
Como a proposta do controle é mostrar diversos campos, é preciso definir as
colunas. Uma das formas mais indicadas de fazer isto quando não há
necessidade de edição dos dados no controle, é aninhar uma GridView dentro
da ListView. A partir da linha 28 estão sendo definidas as colunas que fazem
parte do controle.
Nota:é neste ponto que o WPF se torna uma opção muito interessante para s
e escrever aplicações com visual rico na sua interface, ou seja, a capacidade d
e combinarmos controles diferentes para obtermos a funcionalidade desejad
a.
Inicialmente foram definidas apenas duas colunas (linhas 29 a 35). As
propriedades são usadas da seguinte forma:
• Header define o texto para o cabeçalho de cada coluna.
• Width define a largura.
• [Link] o alinhamento do texto.
• DisplayMemberBinding informa qual o nome da propriedade da coleção de
objetos que serve de fonte de dados, como pudemos observar ao definir o
atributo ItemsSource do controle ListView.
Com estes dados, termina a definição do layout básico da janela principal.
Note que nenhum estilo foi definido ainda. Porém, como está sendo definido o
layout, nenhum dado será enviado. É preciso editar o código em C# para
definir como preencher os dados. A Listagem 5 demonstra o código usado
para isto.
Listagem 5. Populando a janela
1 using [Link];
2 using [Link];
3 using [Link];
4 using [Link];
5 using [Link];
6
7 namespace [Link]
8 {
9 public partial class MainWindow : Window
10 {
11 List<Usuario> usuarios;
12
13 public MainWindow()
14 {
15 InitializeComponent();
16 }
17 private void Init(object sender, [Link] e)
18 {
19 if (usuarios == null)
20 usuarios =
[Link]();
21
22 [Link] = usuarios;
23 }
24 public void listView_DoubleClick(object sender,
MouseButtonEventArgs e)
25 {
26 Usuario usuario = ((ListViewItem)sender).Content as Usuario;
27 UsuarioDados frmUsuario = new UsuarioDados(usuario);
28 [Link]();
29 }
30 }
31 }
Como o objetivo do código é preencher os dados, na linha 11 foi definida uma
lista genérica usando como tipo a classe Usuario definida anteriormente. Para
preencher esta lista foi definido um método chamado Init que foi configurado
na listagem XAML para ser executado assim que a janela é inicializada. Este
método está descrito a partir da linha 17 e simplesmente verifica se a lista está
nula (linha 19), caso esteja, chama o método que retorna os dados na linha 20
e, na linha 22, define estes dados como contexto da janela atual, o que faz com
que o controle ListView possa usar a lista como fonte de dados. Outro ponto
de atenção ao escrever métodos que respondem a eventos dos controles é
cuidar para que a assinatura dos mesmos corresponda aos parâmetros
esperados pelos event handlers. A documentação do framework pode ajuda-lo
a conhecer estes parâmetros, uma vez que cada controle tem um conjunto
específico de parâmetros esperados.
Na linha 24 está definido o método que responde ao evento de duplo clique do
controle ListView. Observe os parâmetros que são usados para este tipo de
método, porque são usados para definir qual o objeto que foi selecionado na
lista. O comportamento esperado é que ao realizar um duplo clique uma janela
exibe os detalhes do objeto. Assim, a linha 26 obtém uma instância da classe
fazendo uma conversão do item que foi passado como argumento no método.
Um ponto de atenção ao fazer este tipo de operação em aplicações
profissionais é primeiramente assegurar-se de que todas as propriedades –
inclusive as que podem ser nulas – estão preenchidas devidamente. Também
deve ser verificado se a conversão poderá ser feita, se não há a possibilidade
de vir um objeto de um tipo diferente.
Com o objeto definido e instanciado, resta criar a janela que vai exibir os
dados (linha 27) e exibir estes dados (linha 28). Usando as marcações que
foram definidas e o código, a aparência inicial da janela fica como a
da Figura 13.
Figura 13. Aparência inicial da janela
Criando a janela para exibir
detalhes dos usuários
Após definir a janela principal, deve ser adicionada uma segunda para definir
e exibir os detalhes do item que foi selecionado na lista.
A Figura 14 ilustra o processo de adicionar uma janela ao projeto através do
menu de contexto do Visual Studio.
Figura 14. Adicionando uma janela ao projeto
Esta ação abre uma janela padrão do Visual Studio, como a da Figura 15, que
permite escolher o tipo de elemento que vai ser adicionado ao projeto.
Figura 15. Inserindo uma nova janela
Novamente é preciso definir o layout desta janela e a Listagem 6 mostra o
código XAML que foi usado.
Listagem 6. Marcação XAML original para a janela
1 <Window x:Class="[Link]"
2
xmlns="[Link]
3 xmlns:x="[Link]
4 Title="Dados do Usuário"
5 Height="300"
6 Width="300">
7 <DockPanel>
8 <Border [Link]="Top"
9 x:Name="bordaUsuario">
10 <TextBlock x:Name="TituloUsuario"
11 Text="Detalhes do usuário" />
12 </Border>
13 <Grid [Link]="Top"
14 x:Name="gridUsuario">
15 <[Link]>
16 <ColumnDefinition Width="auto" />
17 <ColumnDefinition Width="*" />
18 </[Link]>
19 <[Link]>
20 <RowDefinition Height="Auto" />
21 <RowDefinition Height="Auto" />
22 <RowDefinition Height="Auto" />
23 <RowDefinition Height="Auto" />
24 <RowDefinition Height="*"/>
25 </[Link]>
26 <Label x:Name="labelNome"
27 Content="Nome"
28 [Link]="0"
29 [Link]="0"/>
30 <TextBox x:Name="textNome"
31 Text="{Binding Path=Nome}"
32 [Link]="1"
33 [Link]="0" />
34 <Label x:Name="labelLogin"
35 Content="Login"
36 [Link]="0"
37 [Link]="1"/>
38 <TextBox x:Name="textLogin"
39 Text="{Binding Path=Login}"
40 [Link]="1"
41 [Link]="1" />
42 <Label x:Name="labelEmail"
43 Content="E-mail"
44 [Link]="0"
45 [Link]="2"/>
46 <TextBox x:Name="textEmail"
47 Text="{Binding Path=Email}"
48 [Link]="1"
49 [Link]="2" />
50 <CheckBox x:Name="chkAtivo"
51 Content="Ativo"
52 IsChecked="{Binding Path=Ativo}"
53 [Link]="2"
54 [Link]="3" />
55 <Button x:Name="btnOk"
56 Content="_OK"
57 [Link]="2"
58 Height="30"
59 Width="100"
60 [Link]="4"
61 IsDefault="True"
62 IsCancel="True" />
63 </Grid>
64 </DockPanel>
65 </Window>
Esta listagem é um pouco mais extensa do que a da janela principal,
principalmente porque esta janela possui mais controles do que a primeira.
Inicialmente na linha 7 é definido o container principal para os controles,
novamente estou usando um DockPanel. Nas linhas 8 até a 12 novamente
estão contidos os elementos que vão compor o cabeçalho do formulário.
A partir da linha 13 usei um elemento Grid para definir o layout do formulário
e poder aninhar de forma mais fácil os controles. Uma grid funciona de
maneira semelhante às marcações TAB da linguagem HTML e assim você
pode usar linhas e colunas e fazer ajustes em seus alinhamentos e na aparência
dos seus controles. Este elemento Grid está fixado na parte superior da janela
através da propriedade [Link], novamente tendo em vista o auto
ajuste da janela quando esta for redimensionada.
Ao usar a Grid como elemento de layout, é preciso definir as linhas e colunas
(se estiver editando o XAML manualmente, como neste caso). Dessa forma,
da linha 15 à 18 foram definidas as colunas. Para que o tamanho se ajuste ao
conteúdo. A propriedade é definida como auto (linha 16).
Da linha 18 até 25 são definidas as linhas da Grid. É importante lembrar que a
numeração inicia-se com zero e quando os controles forem definidos na
marcação, precisam ter o número da linha e da coluna informados para uma
correta disposição.
A listagem da linha 26 até 49 fazem a definição de como os dados textuais são
exibidos. As principais observações devem recair sobre as propriedades
[Link] e [Link] que definem coluna e linha do elemento Grid
respectivamente para o controle e, para o controle do tipo TextBox o elemento
Text que está usando {Binding} ou seja, uma ligação com um objeto para
obter os dados. Note que o atributo Path passa o nome da propriedade que é
usada para preencher o controle.
Na linha 50 um controle diferente dos TextBoxes é usado. Como o tipo de
dado é do tipo Boolean na classe (propriedade Ativo), o controle mais
indicado é usar um CheckBox. Para poder exibir o estado corretamente deste
controle, a propriedade que precisa ser ligada com o objeto é IsChecked (linha
52).
Além disso, é necessário configurar como os dados serão passados para os
controles. Isto é feito no code behind da classe que é demonstrado
na Listagem 7.
Listagem 7. Code behind da classe
1 using [Link];
2 using [Link];
3
4 namespace [Link]
5 {
6 public partial class UsuarioDados : Window
7 {
8 Usuario _usuario;
9
10 public UsuarioDados(Usuario usuario)
11 {
12 InitializeComponent();
13 _usuario = usuario;
14 [Link] = _usuario;
15 }
16 }
17 }
Uma das formas de preencher os controles da janela é usando uma instância
da classe Usuario, assim na linha 8 este objeto é definido dentro do código da
janela. Em seguida, o construtor default é alterado para receber um objeto
como parâmetro e passar este para o campo definido anteriormente (linha 13).
Por fim, na linha 14, este objeto é definido como DataContext da janela, o que
fará com que os controles possam usa-lo para preencher os controles visuais.
Observe que poderíamos fazer o preenchimento de várias formas, mas, estou
apresentando uma alternativa mais direta, principalmente quando o objetivo
for somente apresentar os dados.
Ao terminar a definição do layout da janela e escrever o código para
preencher os dados e ao executar o projeto, a aparência será como
representada na Figura 16.
Figura 16. Aparência inicial da janela com dados do usuário
Definição dos estilos
Uma das desvantagens que percebo ao definir os elementos da interface,
editando a marcação XAML manualmente em vez de se usar o designer, é que
a aparência inicial das janelas ficam muito fraca. Mas, isto não é um problema
muito sério, principalmente se o desenvolvedor também tiver experiência com
aplicativos Web usando o HTML.
Perde-se algum tempo inicialmente, mas, se ganha em flexibilidade, uma vez
que caso seja preciso mudar o estilo do projeto inteiro, isto pode ser mais
facilmente executado ao se trabalhar com estilos.
Eu gosto de usar os padrões de projeto Web como referência porque são uma
boa forma de comparação e um bom ponto de partida para o entendimento do
funcionamento dos estilos com WPF. Uma das alterações para o estilo é usar
uma imagem no controle ListView vinculada com cada item. Para isto, criei
uma pasta no projeto para separar a imagem do resto do conteúdo. Na Figura
17 estão demonstrados os passos para criar uma pasta no projeto.
Figura 17. Incluindo uma pasta ao projeto
Em seguida, é necessário adicionar a figura que já está localizada em outra
pasta no meu computador. A Figura 18 demonstra o menu de contexto usado
para realizar esta tarefa.
Figura 18. Inserindo imagen no projeto
Ao escolher as opções, o Visual Studio apresenta uma janela onde é possível
navegar pelos arquivos do seu computador e escolher a imagem (Figura 19).
Figura 19. Escolhendo uma imagem
Para definir os estilos para a aplicação, foi usado um tipo de arquivo especial
no projeto, chamado ResourceDictionary. No mesmo, cada estilo está
vinculado com uma chave (Key) que será conectada com os controles visuais
para modificar a sua aparência.
A Figura 20 demonstra o menu de contexto usado para adicionar este
elemento ao projeto.
Figura 20. Incluindo um Resource Dictionary
Na janela que o Visual Studio apresenta (Figura 21) deve se escolher um
nome para o elemento para que o mesmo seja criado no projeto.
Figura 21. Criando um Resource Dictionary para estilos
É possível ter vários destes ResourceDictionary em seu projeto. Para
simplificar, optei por criar apenas um destes. Lembre-se de que em uma
aplicação profissional a tendência é termos um número muito grande de
elementos. Embora eu tenha criado este arquivo na pasta raiz do projeto, é
uma boa prática separar este conteúdo em uma pasta.
O conteúdo deste arquivo pode ser conferido na Listagem 8.
Listagem 8. Estilos definidos
1 <ResourceDictionary
xmlns="[Link]
2
xmlns:x="[Link]
3 <!-- Preenchimento do fundo da borda -->
4 <LinearGradientBrush x:Key="fundoBorda"
5 StartPoint="0.5,0"
6 EndPoint="0.5,1">
7 <GradientStop Color="#008E4E" Offset="0" />
8 <GradientStop Color="#002119" Offset="1" />
9 </LinearGradientBrush>
10 <!-- ListView -->
11 <Style TargetType="{x:Type ListView}"
12 x:Key="listViewStyle">
13 <Setter Property="FontFamily"
14 Value="Courier New" />
15 </Style>
16 <!-- Label -->
17 <Style x:Key="labelStyle"
18 TargetType="{x:Type Label}">
19 <Setter Property="FontFamily"
20 Value="Segoe UI" />
21 <Setter Property="Margin"
22 Value="5" />
23 <Setter Property="VerticalAlignment"
24 Value="Center" />
25 <Setter Property="FontWeight"
26 Value="Bold" />
27 </Style>
28 <!-- TextBox -->
29 <Style x:Key="textBoxStyle"
30 TargetType="{x:Type TextBox}">
31 <Setter Property="FontFamily"
32 Value="Lucida Console" />
33 <Setter Property="Margin"
34 Value="4" />
35 <Setter Property="Height"
36 Value="20" />
37 <Setter Property="VerticalAlignment"
38 Value="Center" />
39 </Style>
40 <!-- TextBlock - Título da janela -->
41 <Style x:Key="textBlockStyle"
42 TargetType="{x:Type TextBlock}">
43 <Setter Property="FontFamily"
44 Value="Arial Black" />
45 <Setter Property="FontSize"
46 Value="18" />
47 <Setter Property="Foreground"
48 Value="White" />
49 <Setter Property="Margin"
50 Value="8" />
51 <Setter Property="VerticalAlignment"
52 Value="Center" />
53 </Style>
54 <!-- Borda do título da janela -->
55 <Style x:Key="borderStyle"
56 TargetType="{x:Type Border}">
57 <Setter Property="Background"
58 Value="{StaticResource fundoBorda}" />
59 <Setter Property="CornerRadius"
60 Value="3" />
61 <Setter Property="BorderThickness"
62 Value="2" />
63 <Setter Property="Margin"
64 Value="2" />
65 <Setter Property="BorderBrush"
66 Value="Gray" />
67 </Style>
68 <!-- Borda do conteúdo -->
69 <Style x:Key="borderGrid"
70 TargetType="{x:Type Border}">
71 <Setter Property="CornerRadius"
72 Value="5" />
73 <Setter Property="BorderThickness"
74 Value="1" />
75 <Setter Property="Margin"
76 Value="4" />
77 <Setter Property="BorderBrush"
78 Value="Green" />
79 </Style>
80 <!-- Controle checkBox -->
81 <Style x:Key="checkBoxStyle"
82 TargetType="{x:Type CheckBox}">
83 <Setter Property="Margin"
84 Value="5" />
85 </Style>
86 </ResourceDictionary>
A estrutura básica é dada pelo Visual Studio, então ao criar um estilo tudo o
que é necessário é conhecer alguns elementos importantes para o que você
deseja fazer.
Nas tags localizadas entre as linhas 4 e 9 foram feitas definições para criar um
efeito de gradiente, que é um dos mais populares dentro do WPF. Este efeito
preenche as coordenadas passadas para o StartPoint e EndPoint com as cores
definidas para na tag GradientStop.
As coordenadas de StartPoint e EndPoint indicam pontos para os eixos X e Y
que variam de 0 a 1. No exemplo, tanto o ponto X de partida como final têm
os mesmos valores, o gradiente vai ser preenchido na horizontal. A Figura
22 é baseada na documentação da Microsoft.
Figura 22. Eixo de preenchimento do Gradiente
O gradiente é preenchido seguindo o eixo traçado. Se for possível, faça
experiências com os StartPoint e EndPoint para obter outros resultados.
Ainda com relação ao preenchimento na tag GradientStop você informa as
cores usando os códigos hexadecimais como se estivesse definindo uma folha
de estilos CSS.
O atributo Offset estabelece o ponto de referência para a cor. Sendo o
primeiro zero, o superior definido em StartPoint e 1, correspondente ao
EndPoint. Como neste exemplo estou trabalhando com duas cores apenas, os
pontos passados em Offset correspondem aos pontos iniciais e finais. É
possível ter um gradiente com várias cores e controlar a mistura destas através
do atributo Offset.
Uma boa prática é ir testando os pontos até atingir o efeito desejado. Para
chegar ao código das cores, uma dica é usar editores de imagens. Alguns
programas são gratuitos como o Paint .NET (veja seção Links), que gera os
códigos em hexadecimal.
Para que o gradiente possa ser usado em outras marcações XAML, foi
definido uma chave para o mesmo na tag x:Key. Fique atento a este aspecto
ao criar os estilos.
O próximo elemento de estilo definido é o que vai ser aplicado para o controle
do tipo ListView e está localizado das linhas 11 até a 14.
Perceba que uma vez compreendida qual a estrutura dos estilos a ser aplicada
em controles, fica intuitiva a sua utilização. Todo os estilos precisarão ter os
elementos básicos como o deste exemplo:
• TargetType – indica qual o tipo de controle que você deseja aplicar o efeito
ou o estilo. Aqui tanto podem ser controles mais específicos como TextBoxes
e Labels como mais abrangentes e que servem de base para outros tipos de
controles como o famoso Control.
• x:Key – usado para definir um identificador para o estilo. Sem este, não se
pode usar o estilo.
Dentro da tag Style podemos ter uma ou várias tags Setter aninhadas. Neste
deve ser passado o nome da propriedade do controle que se deseja formatar e
um valor que seja válido para ser aplicado ao mesmo.
Todos os estilos deste arquivo seguem o mesmo padrão. Preste atenção apenas
no estilo definido a partir da linha 55. Neste foi referenciado o gradiente que
foi criado no mesmo arquivo anteriormente (linha 57) para a definição da
propriedade Background da borda.
Como há pouca variação no código, esta listagem não requer maiores
explicações e podemos dar sequência explicando a próxima alteração a ser
feita no projeto, que é no arquivo [Link] para poder ligar o
ResourceDictionary com a aplicação. A Listagem 9 demonstra como este
arquivo deve ficar após as alterações.
Listagem 9. Alteração [Link]
1 <Application x:Class="[Link]"
2
xmlns="[Link]
3
xmlns:x="[Link]
4 xmlns:res="clr-namespace:[Link]"
5 StartupUri="[Link]">
6 <[Link]>
7 <ResourceDictionary Source="Resource_basico.xaml" />
8 </[Link]>
9 </Application>
A tag [Link] encerra o ResourceDictionary que foi editado
anteriormente.
É possível colocar vários destes arquivos referenciados e também fazer
definições de estilo diretamente no arquivo, porém, é uma boa prática deixar
as definições de estilo separadas como é feito em aplicações Web ao separar
as folhas de estilo CSS.
Nota: embora esteja utilizando sempre a comparação dos estilos em XAML c
om o CSS, é importante notar que são recursos diferentes. Esta serve apenas
para que se tenha uma base, um ponto de partida para poder entender qual
o conceito dos estilos. Folhas de estilo CSS tem muito em comum com os esti
los XAML, mas são dois recursos totalmente distintos, principalmente por qu
e o primeiro depende da interpretação do código feita pelo browser. Já o XA
ML e seus estilos dependem apenas do framework .NET.
Com os estilos definidos, já podemos fazer alterações nas janelas do projeto e
referenciar estes para que os controles possam ser formatados
apropriadamente.
A primeira janela a ser justada é a principal, que tem o seu conteúdo ajustado
demonstrado na Listagem 10. As alterações que foram feitas estão destacadas
no código para ficar mais fácil a sua identificação.
Listagem 10. [Link] na versão final
1 <Window x:Class="[Link]"
2
xmlns="[Link]
3 xmlns:x="[Link]
4 Title="[Link] Janela principal"
5 Height="350"
6 Initialized="Init"
7 Width="525">
8 <[Link]>
9 <Style x:Key="doubleClick"
10 TargetType="{x:Type ListViewItem}">
11 <EventSetter Event="MouseDoubleClick"
12 Handler="listView_DoubleClick" />
13 </Style>
14 </[Link]>
15 <DockPanel>
16 <Border [Link]="Top"
17 Style="{StaticResource borderStyle}"
18 x:Name="bordaPrincipal">
19 <TextBlock x:Name="tituloPrincipal"
20 Style="{StaticResource textBlockStyle}">
21 Listagem de Usuários
22 </TextBlock>
23 </Border>
24 <Border [Link]="Top"
25 Style="{StaticResource borderGrid}"
26 x:Name="bordaListagem">
27 <ListView x:Name="listaUsuario"
28 Style="{StaticResource listViewStyle}"
29 ItemContainerStyle="{StaticResource
ResourceKey=doubleClick}"
30 ItemsSource="{Binding}">
31 <[Link]>
32 <GridView>
33 <GridViewColumn>
34 <[Link]>
35 <DataTemplate>
36 <StackPanel
Orientation="Horizontal">
37 <Image Width="16"
Height="16"
38
Source="imagens\user_sticker_64.png"/>
39 </StackPanel>
40 </DataTemplate>
41 </[Link]>
42 </GridViewColumn>
43 <GridViewColumn Header="ID"
44 Width="100"
45
[Link]="Right"
46
DisplayMemberBinding="{Binding ID}"/>
47 <GridViewColumn Header="Nome"
48 Width="Auto"
49
DisplayMemberBinding="{Binding Nome}"/>
50 </GridView>
51 </[Link]>
52 </ListView>
53 </Border>
54 </DockPanel>
55 </Window>
Nas linhas 17, 20, 25 e 28 o código apenas informa qual a chave de estilo a ser
usada para o controle definido na TAG. Já, para a apresentação dos dados
dentro do controle ListView, foi acrescentada uma coluna para representar
uma imagem estática, baseada em um conteúdo do disco que vai exibir um
ícone para cada item da listagem.
A marcação das linhas 33 até a 43 demonstra um padrão que pode ser adotado
para fazer exibição de imagens. É bem apropriado para este exemplo. Neste
caso foi acrescentado um StackPanel ao item da grid para poder criar uma
coluna com suporte a exibição de imagens.
As definições de estilo para a janela que demonstra os detalhes do usuário
estão exibidas na Listagem 11 e novamente as alterações feitas na marcação
estão em destaque.
Listagem 11. Janela dos detalhes do usuário final
1 <Window x:Class="[Link]"
2
xmlns="[Link]
3 xmlns:x="[Link]
4 Title="Dados do Usuário"
5 Height="300"
6 Width="600">
7 <DockPanel>
8 <Border [Link]="Top"
9 Style="{StaticResource borderStyle}"
10 x:Name="bordaUsuario">
11 <TextBlock x:Name="TituloUsuario"
12 Style="{StaticResource textBlockStyle}"
13 Text="Detalhes do usuário" />
14 </Border>
15 <Border Style="{StaticResource borderGrid}">
16 <Grid [Link]="Top"
17 x:Name="gridUsuario">
18 <[Link]>
19 <ColumnDefinition Width="auto" />
20 <ColumnDefinition Width="*" />
21 </[Link]>
22 <[Link]>
23 <RowDefinition Height="Auto" />
24 <RowDefinition Height="Auto" />
25 <RowDefinition Height="Auto" />
26 <RowDefinition Height="Auto" />
27 <RowDefinition Height="*"/>
28 </[Link]>
29 <Label x:Name="labelNome"
30 Content="Nome"
31 Style="{StaticResource labelStyle}"
32 [Link]="0"
33 [Link]="0"/>
34 <TextBox x:Name="textNome"
35 Style="{StaticResource textBoxStyle}"
36 Text="{Binding Path=Nome}"
37 [Link]="1"
38 [Link]="0" />
39 <Label x:Name="labelLogin"
40 Style="{StaticResource labelStyle}"
41 Content="Login"
42 [Link]="0"
43 [Link]="1"/>
44 <TextBox x:Name="textLogin"
45 Style="{StaticResource textBoxStyle}"
46 Text="{Binding Path=Login}"
47 [Link]="1"
48 [Link]="1" />
49 <Label x:Name="labelEmail"
50 Content="E-mail"
51 Style="{StaticResource labelStyle}"
52 [Link]="0"
53 [Link]="2"/>
54 <TextBox x:Name="textEmail"
55 Style="{StaticResource textBoxStyle}"
56 Text="{Binding Path=Email}"
57 [Link]="1"
58 [Link]="2" />
59 <CheckBox x:Name="chkAtivo"
60 Style="{StaticResource
checkBoxStyle}"
61 Content="Ativo"
62 IsChecked="{Binding Path=Ativo}"
63 [Link]="2"
64 [Link]="3" />
65 <Button x:Name="btnOk"
66 Content="_OK"
67 [Link]="2"
68 Height="30"
69 Width="100"
70 [Link]="4"
71 IsDefault="True"
72 IsCancel="True" />
73 </Grid>
74 </Border>
75 </DockPanel>
76 </Window>
Desta vez apenas foram vinculados aos controles as chaves de estilo criadas
no ResourceDictionary.
Tendo feitas estas modificações, ao executar o projeto ele vai apresentar a
aparência desejada.
Conclusão
Uma das críticas que são feitas para as aplicações WPF e o XAML é que sua
curva de aprendizado é por demais acentuadas, o que acarreta um gasto de
tempo considerável para que os profissionais tenham o domínio que justifique
sua adoção e a mesma produtividade com aquelas ferramentas que já estão
acostumados. Porém, é bom lembrar que com esta tecnologia o objetivo é
separar as responsabilidades, deixando cada parte da aplicação a cargo do
profissional mais indicado.
Assim, se houver algum especialista em design dentre os profissionais que
compõem a equipe de um projeto, ele será o candidato mais forte a trabalhar
com o XAML e, o design da aplicação, principalmente porque se forem
adotados design patterns como o MVVM, ele pode fazer o seu trabalho sem
necessitar alterar o código escrito em C#, deixando isto a cargo dos
responsáveis pelas regras de negócio da aplicação.
Neste artigo eu optei em mostrar todo o trabalho exclusivamente com o
código XAML em vez de usar o design. Primeiramente por causa do motivo já
exposto anteriormente, que é a necessidade de ter de fazer ajustes neste código
para poder obter os refinamentos na interface, que sempre acabam sendo
necessários. Apesar de considerar melhor a edição do código manualmente,
em cenários onde se exige produtividade em índices mais altos, será bem
vinda a utilização de ferramentas que façam o design visual “à moda antiga”.
Existem ferramentas muito boas para este propósito como o Expression Blend
da Microsoft e o KaXaml (gratuito).
Escolha o seu caminho dentro desta nova tecnologia e bom aprendizado.
Links
Windows [Link]://[Link]
Styles tutorial
[Link]
Video: Using Styles to Customize Windows Presentation Foundation Con
trols
[Link]
[WPF] Cool effects with Styles & Triggers
[Link]
Archive for the ‘Styles’ Category
[Link]
Coleção de temas em WPF
[Link]
Styles using triggers in WPF
[Link]
/
Busca por ícones
[Link]
KaXaml
[Link]
[Link]
[Link]
por Vladimir Rech
Guru .net e tecnologias MS
Exceções
O artigo explana o tratamento de erros em
aplicações Web construídas na plataforma
.NET. O tratamento de exceções é muito
importante, tanto para questão de
segurança como para a personalização de
erros inesperados ao usuário final.
De que se trata o artigo
O artigo explana o tratamento de erros em aplicações Web construídas na
plataforma .NET. O tratamento de exceções é muito importante, tanto para
questão de segurança como para a personalização de erros inesperados ao
usuário final.
Em que situação o tema é útil
O tema é útil em situações que seja necessário tratar os principais erros que a
aplicação possa gerar ao usuário final. Desta forma, todo erro será
perfeitamente entendido por parte do usuário. Além disso, quando você deseja
tratar a segurança da aplicação, o tratamento de exceções se torna muito
importante.
Exceções em soluções Web - Tratando erros em aplicações [Link]
Web Forms
O tratamento de erros em aplicações concebidas na plataforma .NET é feito a
partir da manipulação de exceções. Estas representam objetos contendo
informações descrevendo as falhas ocorridas durante a execução de um
sistema. No caso específico de aplicações Web, o .NET framework
disponibiliza uma série de recursos em projetos baseados nas tecnologias
[Link] Web Forms, garantindo assim um tratamento de erros eficaz em
sites gerados a partir das mesmas.
Na edição anterior foi iniciada uma discussão sobre o tratamento de erros em
aplicações .NET. É praticamente certo que falhas ocorrerão ao longo da
execução de um sistema. No entanto, existem meios de coibir ou atenuar o
impacto de tais problemas, os quais são representados por objetos conhecidos
como exceções.
Boa parte dos conceitos envolvendo a manipulação de exceções é aplicável a
qualquer tipo de solução construída na plataforma .NET. Construções como
blocos de código envolvendo as instruções try-catch-finally exemplificam
bem este aspecto, servindo de base para a elaboração de funcionalidades que
capturam e efetuam uma série de procedimentos diante de um erro.
Uma das principais formas de desenvolvimento de sites em .NET, consiste no
uso do [Link] Web Forms. A finalidade deste artigo é abordar como
exceções podem ser tratadas em sites construídos baseados nesta tecnologia.
Para isto, serão apresentados diversos exemplos envolvendo o uso de recursos
do Web Forms, de forma a explorar assim as diferentes técnicas oferecidas
pela plataforma .NET na manipulação de exceções em aplicações Web.
Tratamento de erros em aplicações
[Link] Web Forms
A tecnologia Web Forms integra o [Link] desde os primórdios da
plataforma .NET, correspondendo a uma alternativa que preza a produtividade
a partir do uso de numerosos controles visuais. Estes componentes foram
disponibilizados com o objetivo de transformar a construção de páginas para
Web em uma tarefa extremamente simples, estando em conformidade com o
paradigma conhecido como RAD (Rapid Application Development).
O padrão Web Forms pode ser encarado como o equivalente, quando
considerado o mundo das soluções Web, da mesma abordagem empregada em
soluções baseadas em Windows Forms. Em ambos os casos, componentes
visuais podem disparar diversos tipos de eventos, com métodos definidos
dentro da aplicação sendo responsáveis pelo tratamento das ações
equivalentes.
Páginas Web num projeto Web Forms são estruturas que se baseiam na classe
Page (namespace [Link]), contando com um arquivo de extensão
.aspx em que estão declarações em HTML e componentes visuais, além um
segundo arquivo com o código-fonte (no caso de algo criado em C#, a
extensão neste caso será .cs).
Alguns exemplos de controles comumente utilizados em projetos [Link]
Web Forms são:
• Button: botão para a execução de opções em páginas Web;
• DropDownList: caixa para a seleção de opções;
• HyperLink: corresponde a um link em uma página Web;
• TextBox: representa um campo texto em um formulário HTML.
Nota do DevMan
A metodologia RAD (Rapid Application Development) prioriza a construção
de interfaces gráficas que possam ser modeladas facilmente a partir de uma fer
ramenta gráfica (no caso específico da plataforma .NET, o Visual Studio). Alé
m disto, controles visuais são utilizados em larga escala, aliando-se a isto conc
eitos de Orientação a Objetos e da programação orientada a eventos. Tudo isto
leva em conta a preocupação constante por tornar mais ágil e flexível o proces
so de desenvolvimento de novas aplicações.
Erros podem ser tratados numa aplicação Web Forms das seguintes maneiras:
• Através de blocos try-catch-finally definidos dentro do próprio evento em
que pode acontecer uma falha;
• Por meio do evento Page_Error implementado dentro de uma página .aspx;
• No evento Application_Error declarado no arquivo [Link];
• Utilizando páginas de erro customizadas e declaradas dentro do [Link].
Cada uma destas práticas pode ser mais indicada a um determinado tipo de
demanda, competindo aos desenvolvedores envolvidos em um projeto optar
pelas alternativas que melhor se adaptam à solução que está sendo construída.
A seguir, serão detalhados exemplos de uso das diferentes opções para
tratamento de erros em aplicações [Link] Web Forms.
Os diversos exemplos descritos a seguir fazem uso da classe LogErro, cujo
código é demonstrado na Listagem 1. Este tipo tem por finalidade gravar
informações de erros que venham a ocorrer em uma aplicação num
mecanismo de log (arquivo ou base de dados), com este tipo de ação sendo
executado a partir da invocação do método RegistrarErro.
A operação RegistrarErro recebe como parâmetro um objeto do tipo
Exception, empregando o mesmo na obtenção das seguintes informações:
• A mensagem relativa ao erro que estará sendo registrado (propriedade
Message da instância “ex”);
• As informações de Stack Trace contidas na exceção fornecida como
parâmetro (propriedade StackTrace);
• O nome do tipo que representa a exceção, sendo que para isto é acessado o
método GetType. A invocação deste tipo retorna uma instância da classe
Type, a qual contém informações referentes à classe através da qual foi gerada
uma instância daquela exceção específica. Por fim, o acesso à propriedade
Name retorna o nome do tipo em que se baseia a exceção;
• O nome da provável exceção que foi a causa do original do problema, sendo
que a instância relativa a este erro poderá estar contida em outro objeto que
representa uma exceção (na propriedade InnerException do mesmo). A
identificação da classe que corresponde à exceção vinculada à propriedade
InnerException também depende do uso do método GetType e,
posteriormente, da propriedade Name presente na referência devolvida por
esta operação.
Listagem 1. Exemplo de classe para a gravação de informações de erro
using System;
using [Link];
using [Link];
using [Link];
namespace TesteExceptions
{
public static class LogErro
{
public static void RegistrarErro(Exception ex)
{
string mensagemErro = [Link];
string stackStrace = [Link];
string tipoException = [Link]().Name;
string tipoInnerException = null;
if ([Link] != null)
{
tipoInnerException =
[Link]().Name;
}
// Gravação das informações de log em um banco de dados
// ou arquivo criado especificamente para este fim
...
}
}
}
O tipo Exception (namespace System) corresponde, ao se considerar a
plataforma .NET como um todo, à estrutura básica da qual derivam todas as
demais representações de erro. Esta classe conta com informações que
permitem não apenas identificar a ocorrência de um determinado tipo de falha,
como também trilhar a possível origem do problema. É importante frisar ainda
que instâncias de Exception são a base para a codificação de blocos para
tratamento de erros, com estes tipos de construções envolvendo o uso de
instruções try e catch.
A seguir são listadas algumas das principais propriedades de Exception. Tais
elementos são empregados com frequência na implementação de mecanismos
para o registro de falhas (log):
• Message: mensagem que descreve a falha associada a uma exceção;
• InnerException: uma nova exceção pode ser lançada diante de uma condição
específica. Nestas situações, é de costume que o erro original seja associado à
mesma, de maneira que seja possível a partir disto rastrear informações da
falha que aconteceu no início do processo. A propriedade InnerException
existe justamente para este fim: se preenchido, este membro da classe
Exception retornará uma instância do erro que serviu de base para o
lançamento da exceção ativa;
• StackTrace: mantém uma lista de todos os métodos que se encontravam em
execução no momento em que uma exceção foi disparada. Trata-se de uma
fonte de informação bastante útil para a gravação de registros de log, uma vez
que este recurso pode auxiliar no rastreamento de uma falha ao longo de uma
aplicação;
• Source: devolve o nome da aplicação ou objeto causador do problema;
• TargetSite: retorna o nome do método em que ocorreu o lançamento da
exceção atual.
Nota do DevMan
Uma classe estática é um tipo que não pode ser instanciado, ou seja, por defini
ção impede-se que instâncias de objetos sejam criadas a partir dessa estrutura.
Métodos declarados neste tipo de construção também são estáticos, com tais o
perações estando normalmente voltadas ao processamento de ações que não e
nvolvam o armazenamento de informações de estado (e que estariam vinculad
as obrigatoriamente a um objeto da classe considerada).
A Figura 1 apresenta um exemplo de aplicação em Web Forms criada com o
objetivo de calcular o IMC (Índice de Massa Corpórea) de uma pessoa. Isso
acontecerá a partir do preenchimento dos campos de Peso e Altura e, na
sequência, através da execução da opção “Calcular IMC”. Se por ventura
ocorrerem erros durante o processamento desta funcionalidade, com nenhum
procedimento de tratamento de falhas tendo sido implementado, uma tela
similar à que consta na Figura 2 será apresentada ao usuário.
Figura 1. Tela para cálculo do IMC de uma pessoa
Figura 2. Erro que aconteceu durante o cálculo do IMC
Embora o formulário padrão descreva que um problema aconteceu, o mesmo
pode ser confuso para um usuário sem grandes conhecimentos em informática
ou, até mesmo, passar uma falsa impressão de que a aplicação não é estável
(ou que diversos problemas existem na mesma). Uma boa prática neste caso é
implementar um mecanismo que capture exceções ocorridas numa aplicação
[Link], garantindo que uma mensagem mais amigável seja apresentada ao
usuário em tela.
Outra motivação que costuma contribuir para a implementação de uma
mensagem customizada de erro é, em muitos casos, a preocupação com a
segurança da aplicação. As informações constantes na página de erros do
[Link] pode servir de embasamento para ataques, já que listam uma série
de detalhes técnicos relativos à falha (por exemplo, alguns trechos de códigos
da classe). Usuários que contem com bons conhecimentos e agindo de má fé
viriam então a explorar prováveis vulnerabilidades.
Nota: Para efeitos de simplificação, os diversos exemplos demonstrados ao l
ongo deste artigo omitiram o código das páginas .aspx utilizadas. Isto aconte
ceu porque o foco principal é a discussão em torno dos diferentes mecanism
os de tratamento de erros em aplicações [Link].
A Listagem 2 apresenta um exemplo de manipulação de exceções em um
bloco try-catch. Este procedimento de tratamento de falhas acontece dentro do
evento Click (método btnCalcularIMC_Click) de um controle Button de nome
“btnCalcularIMC”.
O método btnCalcularIMC_Click possui o seguinte funcionamento:
• Inicialmente são convertidos o peso e a altura informados por uma pessoa,
empregando a classe Convert e o método ToDecimal definido nesta estrutura
(a fim de converter texto para um número no tipo decimal);
• Caso ocorram erros durante o processo de conversão, a cláusula catch que
captura a exceção do tipo FormatException é acionada. Dentro desta, uma
mensagem que indica o erro de conversão é atribuída ao Label de nome
“lblErroCalculoIMC”, além da invocação que é feita à operação RegistrarErro
definida na classe LogErro (objetivando com isto registrar o problema em um
arquivo ou banco de dados de log);
• Se acontecer um erro de divisão por zero durante o cálculo do IMC, uma
exceção do tipo DivideByZeroException será lançada. O bloco catch
correspondente é acionado, de maneira que se informe ao usuário este
problema, assim como seja gravada a ocorrência no log;
• Na eventualidade da exceção não ser uma FormatException ou
DivideByZeroException, o bloco catch que captura uma exceção do tipo
Exception é acionado. Esta prática permite também que exceções não
previstas nos blocos anteriores também possam ser tratadas.
Listagem 2. Exemplo de um trecho de código empregando try-catch
...
protected void btnCalcularIMC_Click(object sender, EventArgs e)
{
try
{
decimal peso = [Link]([Link]);
decimal altura = [Link]([Link]);
decimal valorIMC = peso / (altura * altura);
[Link] = [Link]();
if (valorIMC < 18)
[Link] = "Abaixo do Peso";
else if (valorIMC > 25)
[Link] = "Acima do Peso";
else
[Link] = "Peso Normal";
}
catch (FormatException exFormatacao)
{
[Link] =
"Valor(es) inválido(s) para Peso e/ou Altura.";
[Link](exFormatacao);
}
catch (DivideByZeroException exDivisaoZero)
{
[Link] =
"Erro de divisão por zero durante o cálculo do IMC.";
[Link](exDivisaoZero);
}
catch (Exception ex)
{
[Link] =
"Erro durante o cálculo do IMC.";
[Link](ex);
throw ex;
}
}
...
Caso um usuário forneça valores inválidos para o cálculo do IMC, uma tela
como a que consta na Figura 3 será exibido ao mesmo. O formulário
apresentado corresponde a um exemplo de tratamento de erro dentro da
própria em que aconteceu a falha, com o Label lblErroCalculoIMC tendo seu
conteúdo preenchido com uma mensagem que descreve o problema.
Figura 3. Tratamento de erro dentro da própria página em que ocorreu um
problema
Blocos try-catch representam uma das principais formas de tratamento de
erros dentro da plataforma .NET. Este tipo de construção já foi abordado no
artigo anterior, sendo importante relembrar o seguinte:
• O conjunto de instruções que pode levar ao lançamento de uma exceção
deve constar no bloco try. Se uma falha ocorrer ao longo do processamento
sequencial das diversas instruções existentes, o fluxo de execução é desviado
para os blocos contendo instruções catch.
• As exceções declaradas em blocos catch devem ser ordenadas
preferencialmente do tipo mais específico para o mais genérico. O mecanismo
de execução do .NET (runtime) analisa as diversas instruções, capturando
exceções em instruções, na ordem em que essas estruturas aparecem no
código. Caso encontre alguma exceção baseada num tipo ou supertipo (classe
básica) do erro gerado, a busca é interrompida, ocorrendo então o
processamento das instruções contidas dentro bloco catch correspondente.
• É de costume que instruções capturando o tipo Exception venham por último
num bloco de tratamento de erros, já que esta classe está no topo da hierarquia
de exceções na plataforma .NET. Esta prática garante que erros sempre serão
analisados e, possivelmente, tratados em blocos try-catch.
• Se nenhuma cláusula catch for acionada, a exceção é propagada para outras
estruturas da aplicação em questão. Isto acontecerá, basicamente, quando um
erro não previsto por um bloco try-catch acontecer durante a execução de uma
aplicação.
• É possível que uma instrução catch não contenha nenhuma indicação do tipo
de exceção a ser capturado. Em tais casos, o runtime por convenção assumirá
que qualquer exceção ainda não capturada estará sendo tratada por tal bloco
(algo equivalente a um bloco que captura o tipo Exception).
Já a classe Convert é uma estrutura empregada na conversão de dados de um
tipo para outro. Além do método ToDecimal, Convert disponibiliza outras
operações como ToInt32 (conversões para números inteiros), ToDouble
(conversões para números com casas decimais), ToDataTime (conversões
para datas) etc.
Nota do DevMan
FormatException é um tipo de exceção disparado quando o valor informado c
omo parâmetro a um método não se encontra no formato esperado. Trata-se de
um erro bastante comum em conversões que envolvam a conversão de um tipo
de dado para outro.
Já a exceção DivideByZeroException é lançada em operações aritméticas que
envolvam a divisão de um número por zero. Do ponto de vista convencional d
a Matemática, tal divisão é impossível, cabendo ao .NET gerar um erro em ten
tativas de processamento de instruções que envolvam um numerador zerado.
A Listagem 3 exibe outra implementação possível para o método
btnCalcularIMC_Click, só que desta vez foram retiradas as instruções try-
catch. O objetivo disto é demonstrar, em termos gerais, como erros disparados
pelo evento Click (e que não foram tratados a partir dessa estrutura) seriam
tratados a partir das alternativas descritas adiante.
Listagem 3. Exemplo de um método que pode gerar exceções e que não
emprega try-catch
...
protected void btnCalcularIMC_Click(object sender, EventArgs e)
{
decimal peso = [Link]([Link]);
decimal altura = [Link]([Link]);
decimal valorIMC = peso / (altura * altura);
[Link] = [Link]();
if (valorIMC < 18)
[Link] = "Abaixo do Peso";
else if (valorIMC > 25)
[Link] = "Acima do Peso";
else
[Link] = "Peso Normal";
}
...
Na Listagem 4 é apresentado um exemplo de uso do evento Page_Error.
Quando acontecer um erro e o mesmo não for tratado em uma página de um
projeto Web Forms, este método (Page_Error) será automaticamente acionado
pelo engine de execução do [Link] (partindo da premissa que o mesmo
tenha sido implementado no formulário em questão).
A exceção que ocasionou a falha na página é obtida por meio da invocação do
método GetLastError, a partir do objeto vinculado à propriedade Server. Em
seguida, invoca-se a operação RegistrarErro do tipo LogErro, com o intuito de
gravar informações relativas ao problema corrente. Por fim, o método
Transfer do objeto Server é acionado, encerrando a execução do formulário
atual e exibindo em seu lugar uma página de erro customizada ([Link]).
Listagem 4. Exemplo de implementação do evento Page_Error em [Link]
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
namespace TesteExceptions
{
public partial class _Default : [Link]
{
...
protected void Page_Error(object sender, EventArgs e)
{
// Capturando e registrando a exceção em log
Exception ex = [Link]();
[Link](ex);
// Transferindo o fluxo para uma página de erro
[Link]("~/[Link]");
}
...
}
}
Páginas comuns na tecnologia Web Forms são estruturas derivadas da classe
Page (namespace [Link]). Além de Page_Error, este tipo conta com
diversos outros eventos que controlam os diferentes momentos pelos quais
passa uma página Web, podendo-se destacar alguns exemplos:
• Page_PreInit: executado antes dos controles de um formulário terem sido
inicializados. Dentro deste evento é possível adicionar componentes criados
dinamicamente à página em questão;
• Page_Init: disparado após todos os controles terem sido inicializados.
Utilizado normalmente para a configuração de valores de propriedades de
componentes de uma página;
• Page_Load: representa a fase em que uma página começa a ser carregada,
sendo geralmente usado para a atribuição de valores a propriedades de
componentes. Importante lembrar que em muitas ocasiões são acessadas
inclusive informações armazenadas em bancos de dados relacionais;
• Page_PreRender: evento empregado para a realização de ajustes finais ao
conteúdo de uma página. Isto acontece no momento anterior ao processo de
renderização (geração da visualização de uma página).
A propriedade Server é parte integrante do tipo Page, armazenando como
conteúdo uma instância baseada na classe HttpServerUtility. Já este último
tipo disponibiliza várias funcionalidades para a manipulação de requisições
dentro de páginas criadas no modelo Web Forms. A seguir são listados alguns
métodos de HttpServerUtility comumente utilizados no desenvolvimento de
soluções Web:
• GetLastError: retorna um objeto que representa o erro atual e que não foi
tratado até aquele momento;
• ClearError: elimina as informações relativas a um erro atual e que estão em
memória, evitando assim que a exceção correspondente seja propagada para
outros pontos da aplicação;
• HtmlEncode: quando uma sequência de texto possuir caracteres como os
sinais de maior (“>”) e/ou menor (“<”), o browser que estiver sendo
empregado interpretará isto como parte de uma tag HTML. O método
HtmlEncode converte esses caracteres para strings específicas de HTML (“<”
para o sinal de menor, “>” para maior), evitando assim prováveis erros na
visualização da página considerada;
• MapPath: retorna o endereço físico de um arquivo, tomando como base o
caminho virtual do mesmo dentro da solução Web;
• Transfer: encerra a execução do formulário atual, redirecionando o usuário
para uma nova página; importante ressaltar que o endereço original da URL
em que ocorreu o problema será mantido ao se empregar a operação Transfer.
A Figura 4 demonstra um exemplo de erro ocorrido durante o processamento
do cálculo do IMC, com o evento Page_Error redirecionando o fluxo para a
visualização do conteúdo presente na página [Link].
Figura 4. Página de erro exibida após a execução do evento Page_Error
Nota do DevMan
O método HtmlEncode disponibilizado pelo objeto Server pode contribuir no
sentido de evitar ataques do tipo XSS (Cross Site Scripting). Ações deste tipo
são comuns em sites que contem com campos para a digitação de informações
, com usuários mal intencionados informando código em Javascript e element
os HTML no conteúdo de um campo (ao invés de dados num formato esperad
o pela aplicação em questão). Como a operação HtmlEncode converte caracter
es que formariam uma tag HTML, consegue-se assim coibir a inserção de scri
pts maliciosos a partir de formulários de uma solução Web.
A Listagem 5 demonstra como pode ser feita a manipulação de exceções a
partir do evento Application_Error do [Link]. Novamente, o erro atual é
recuperado em uma variável através do método GetLastError da propriedade
Server (a classe Global também conta com uma referência para
HttpServerUtility), com a falha correspondente sendo posteriormente
registrada através do tipo LogErro.
Listagem 5. Utilizando o evento Application_Error do arquivo [Link]
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
namespace TesteExceptions
{
public class Global : [Link]
{
...
void Application_Error(object sender, EventArgs e)
{
// Capturando e registrando a exceção em log
Exception ex = [Link]();
[Link](ex);
// Transferindo o usuário para uma página de erro
[Link]("~/[Link]");
}
...
}
}
O evento Application_Error do arquivo [Link] é outra alternativa que
pode ser utilizada para o controle de erros em uma aplicação [Link] Web
Forms. Sempre que um erro for disparado e não passar por um tratamento
adequando em nenhum outro ponto de um sistema Web, o método
Application_Error será acionado. Graças a este recurso do [Link] torna-
se possível a implementação de um mecanismo centralizado para a
manipulação de exceções, evitando assim a duplicação de um mesmo tipo de
código por todo um projeto (algo que poderia acontecer com diversas páginas
e o uso do evento Page_Error).
O arquivo [Link] contém diversos métodos que são acionados como
resposta à ocorrência de algum evento dentro de uma aplicação [Link].
Além do método Application_Error já descrito anteriormente, a classe
implementada neste arquivo conta com outros eventos como:
• Application_Start: disparado quando a aplicação é inicializada no servidor
Web em que se encontra hospedada;
• Application_End: executado quando se procede com a finalização do
processo que corresponde a um sistema dentro de um servidor Web;
• Session_Start: este é o primeiro evento a ser executado quando um usuário
acessar o site e não possuir ainda uma sessão ativa no servidor da aplicação;
• Session_End: operação disparada quando um usuário efetuar logoff ou
ainda, a sessão do mesmo expirar após um período de inatividade no acesso à
funcionalidades do site considerado.
A última forma para tratamento de erros em aplicações concebidas a partir da
tecnologia Web Forms consiste, basicamente, na inclusão de itens de
configuração no arquivo [Link]. A Listagem 6 apresenta um exemplo
disto.
O elemento customErrors configurado com o atributo mode igual a “On” ativa
este recurso. Já o atributo defaultRedirect indica qual página será exibida
quando da ocorrência de um erro não tratado em nenhum outro ponto do
sistema; no caso, definiu-se que será exibida a página [Link], sendo que
uma exceção não capturada causará o redirecionamento automático para esta
Web Page.
Já o item error com o atributo statusCode preenchido com o valor “404”
determinada a exibição da página [Link] (indicada no
atributo redirect), sendo que isto acontecerá na hipótese de um usuário
informar uma URL inválida durante o acesso ao site.
Listagem 6. Configurando a exibição de páginas de erro no arquivo
[Link]
<?xml version="1.0"?>
<configuration>
...
<[Link]>
...
<customErrors mode="On" defaultRedirect="~/[Link]">
<error statusCode="404" redirect="~/[Link]"
/>
</customErrors>
...
</[Link]>
...
</configuration>
O elemento customErrors representa a base para a definição de erros
customizados a partir do arquivo [Link]. É por meio do atributo mode
deste elemento que é definido se esse recurso estará ativo ou não em um site,
sendo que esta configuração pode assumir os seguintes valores:
• On: indica que a exibição de páginas customizadas de erro acontecerá em
qualquer situação, sempre que uma exceção não for tratada pela aplicação;
• Off: desativa a exibição de páginas de erro customizadas a partir de
configurações que constem no [Link];
• RemoteOnly: define que erros customizados serão exibidos apenas para
usuários remotos. O acesso a partir do servidor em que se encontra a aplicação
não contará com o recurso de exibição de páginas customizadas de erro, sendo
esta configuração útil para que administradores de um sistema possam rastrear
erros que estejam acontecendo no mesmo.
Já o elemento-filho error é de uso opcional dentro de customErrors. Esta
configuração normalmente se baseia em códigos de erro retornados pelo
servidor em que se encontra a aplicação. Cada um dos valores devolvidos por
Web Server corresponde a uma falha específica, com a especificação
acontecendo no atributo statusCode; a página a ser exibida para um usuário é
definida no atributo redirect.
Alguns valores possíveis para statusCode são:
• 403: o usuário em questão não possui permissões para o acesso a uma
determinada página (ou outro tipo de recurso) presente no site;
• 404: não foi possível encontrar a página ou recurso informado;
• 500: ocorreu um erro interno no servidor durante o processamento de uma
requisição.
Nota doDevMan
Arquivos de configuração (extensão .config) são estruturas baseadas em XML
empregadas extensivamente por aplicações .NET. Considerando especificame
nte aplicações Web, sistemas deste tipo contam geralmente com um arquivo
[Link] em seu diretório-raiz, sendo que em tal estrutura constarão config
urações que se aplicam a todos e aos diferentes elementos que constituem aqu
ele site.
Na Figura 5 é demonstrado um exemplo de uma página que seria exibida
quando ocorresse uma falha de código 404 (recurso não encontrado). Este
teste partiu da premissa que a exibição de erros customizados foi ativada na
aplicação Web em execução.
Figura 5. Indicação de que a página requisitada não foi encontrada
Outras considerações sobre a
manipulação de Exceções em .NET
Alguns cenários específicos dentro da plataforma .NET são mais propensos a
erros:
• O acesso a informações presentes em um banco de dados é um exemplo
notório disto. No caso específico do SQL Server em conjunto com a
tecnologia [Link], erros podem acontecer através do uso de um objeto de
conexão do tipo SqlConnection e de estruturas que dependam deste. Uma
exceção baseada na classe SqlException (namespace [Link])
será lançada caso uma falha ocorra;
• A manipulação de arquivos empregando as classes do namespace [Link]
é outra situação que pode conduzir a problemas em tempo de execução. Uma
exceção do tipo IOException (namespace [Link]) seria lançada em tais
cenários;
• A utilização de Web Services também está bastante sujeita a erros, muitas
vezes devido a indisponibilidades ou problemas na comunicação com serviços
remotos. E se tratando do uso específico da tecnologia WCF, uma exceção do
tipo CommunicationException (namespace [Link]) poderá ser
retornada à aplicação que consome um determinado Web Service.
Nota do DevMan
WCF (Windows Communication Foundation) é uma tecnologia integrante da
plataforma .NET empregada na construção de soluções baseadas em serviços.
Um serviço deve ser compreendido como um componente de software que dis
ponibiliza uma série de funcionalidades, as quais poderão vir a ser consumidas
por outras aplicações. Web Services representam um exemplo deste tipo de est
rutura, correspondendo a serviços possíveis de acessar a partir de uma rede (in
tranet ou a própria Internet).
Na Listagem 7 é demonstrado um exemplo de uso da classe SqlConnection e
que poderá resultar no lançamento de uma exceção (uma provável causa seria
a execução de um comando SQL referenciando uma tabela inválida). Caso a
conexão esteja aberta, um erro impedirá que o fluxo normal que consta no
bloco try continue. Já a execução do bloco finally garante que a conexão em
questão será fechada.
Listagem 7. Exemplo de bloco try-finally
...
SqlConnection conexao = new SqlConnection(connectionString);
try
{
[Link]();
// Execução de comandos SQL baseando-se
// no objeto conexao
}
finally
{
[Link]();
}
...
O bloco finally assegura que as instruções contidas no interior do mesmo
serão sempre executadas. Isto acontece independentemente de uma falha
ocorrer ou não longo do processamento das operações contidas num bloco try
associado a esta construção. O uso do comando finally é opcional, sendo que é
possível a montagem de blocos formados por:
• Uma cláusula try e outra finally;
• Uma cláusula try e uma ou mais instruções catch;
• Uma cláusula try, uma ou mais instruções catch e, por fim, o comando
finally.
Um exemplo equivalente ao que consta na Listagem 7 pode ser obtido através
da utilização da instrução using. Na Listagem 8 está o código-fonte
correspondente. Acontecendo ou não uma falha ao longo do bloco using, a
conexão representada pela instância do objeto SqlConnection será
automaticamente liberada (ao final do processo ou diante da ocorrência de um
erro).
Listagem 8. Exemplo de bloco using
...
using (SqlConnection conexao = new SqlConnection(connectionString))
{
// Execução de comandos SQL baseando-se
// no objeto conexao
}
...
Blocos using recebem em sua declaração uma instância de um objeto. Esta
referência será utilizada posteriormente pelas instruções que se encontram
dentro de using. Importante ressaltar que apenas objetos cuja classe
implemente a interface IDisposable poderão ser empregados com este tipo de
construção. Esta característica garante que recursos associados ao objeto em
questão serão sempre liberados (com ou sem erro), já que a execução do bloco
using encerra-se com o acionamento da operação Dispose (definida em
IDisposable) sobre o objeto indicado no início da sequência de comandos.
Nota do DevMan
Interface é um tipo de recurso que define um conjunto de funcionalidades, sen
do normalmente implementadas por classes. Declarações de métodos, proprie
dades e/ou eventos podem fazer parte de interfaces. Uma interface correspond
e, em termos conceituais, a uma espécie de "contrato" ou "esqueleto", servind
o de base para a construção de uma ou mais classes. Já uma classe pode imple
mentar ao mesmo tempo mais de uma interface (a herança de múltiplas classes
, contudo, não é possível dentro da plataforma .NET).
Conclusão
A ocorrência de erros é algo certo ao longo do ciclo de vida de uma aplicação,
por mais crítica que ela possa ser. Um fato que diferencia soluções bem
projetadas daquelas com problemas estruturais é, justamente, a existência de
mecanismos para o controle de falhas. Uma série de recomendações pode ser
aplicada em um software com o intuito de evitar ou até mesmo, atenuar as
consequências de erros em tempo de execução.
No transcorrer deste artigo foram demonstradas as diversas maneiras para a
implementação de técnicas de tratamento de exceções em aplicações
[Link] Web Forms. A escolha pela melhor opção dependerá sempre de
características que serão próprias de cada projeto, cabendo ao time de
desenvolvimento analisar os prós e contras de cada tipo de prática disponível.
Links
[Link]
[Link]
Common Exception Types
[Link]
Displaying a Custom Error Page
[Link]
-page-cs
Exceções custam ou não custam? Uma avaliação empírica
[Link]
aliacao-empirica/
Exception Class
[Link]
spx
Exception Handling
[Link]
Exception Handling Best Practices in .NET
[Link]
s-in-NET
por Renato Jose
Guru .net e tecnologias MS
Serialização
A finalidade deste artigo é apresentar a
técnica de manipulação de objetos
conhecida como serialização. A mesma é
um processo em que se converte o estado
de um objeto para um formato que permita
posteriormente sua recuperação.
De que se trata o artigo
A finalidade deste artigo é apresentar a técnica de manipulação de objetos
conhecida como serialização. A mesma é um processo em que se converte o
estado de um objeto (informações associadas a este) para um formato (que
será visto no decorrer do artigo) que permita posteriormente sua recuperação.
Em que situação o tema é útil
O tema é útil para operações que envolvam a gravação dos dados vinculados a
um objeto em arquivos (para um uso futuro), como também a geração de
pacotes em que a instância correspondente é formatada em um padrão
específico para ser transmitida de uma aplicação para outra (através de uma
rede, por exemplo).
Serialização - Gravando e recuperando a qualquer momento o estado de
um objeto
A gravação de informações para uso posterior representa um tipo de
necessidade bastante comum nos mais variados tipos de programas. Em
muitos casos, por se procurar manter a simplicidade de uma aplicação, a
utilização de um banco de dados é descartada. O processo conhecido como
serialização disponibiliza alternativas para tais situações, já que este tipo de
técnica permite salvar o conteúdo de um objeto para um arquivo, com a
possibilidade ainda de recuperar tais dados num momento oportuno.
Utilitários como MP3 Player, aparelhos telefones entre outros, geralmente
permitem muitas vezes a gravação de preferências e, até mesmo, mantêm um
pequeno repositório de informações que são preservadas e recuperadas em
cada ocasião na qual o programa em questão for acionado. A possibilidade de
manter certos estados oferece a um usuário a chance de customizar certos
comportamentos (como alguma preferencia do sistema em si) do software,
sem que isto implique na perda de configurações que já haviam sido
preenchidas.
Diferente de sistemas corporativos, os quais são projetados para operar em um
contexto caracterizado por um grande volume de informações, aplicações
mais simples costumam dispensar o uso de bancos relacionais. Os dados
necessários à utilização de tais programas são normalmente gravados em
arquivos, com essas estruturas servindo não apenas como meio para
armazenamento, como também ponto de obtenção de informações
processadas anteriormente.
Linguagens de programação baseadas em conceitos da Orientação a Objetos
contam geralmente com alternativas para a implementação de funcionalidades
desse gênero. Referências para uma determinada classe são geralmente
convertidas para algum meio de armazenamento, visando à recuperação dos
objetos correspondentes num momento futuro. O .NET Framework e Java são
plataformas nas quais este tipo de técnica é suportado.
Ao procedimento de conversão de um objeto em um formato que permita sua
reutilização futura dá-se o nome de serialização. A operação inversa a isto é
conhecida como desserialização, sendo caracterizada pela geração de uma
instância idêntica à referência original (incluindo todas as informações
presentes nesta última).
Além da geração e consumo de arquivos por executáveis, a serialização é uma
prática bastante utilizada na transmissão de objetos de um sistema a outro.
Web Services constituem um bom exemplo disto, com os dados presentes em
um objeto sendo organizados em um formato que viabiliza a transferência do
serviço para uma aplicação que consome o mesmo.
Este artigo tem por finalidade demonstrar como o processo de serialização
pode ser implementado em .NET. Para isto, será criada uma aplicação na qual
se emprega esse tipo de recurso, com os arquivos resultantes servindo para
recuperar instâncias exatamente iguais aos objetos em que os mesmos se
basearam.
Nota do DevMan
O paradigma de desenvolvimento de software conhecido como Orientação a O
bjetos gira em torno dos conceitos de classe e objeto.
Objetos são representações dentro de um software para elementos presentes n
o mundo real. Um objeto conta normalmente com atributos e comportamentos
. Atributos devem ser compreendidos como características que identificam um
objeto. Já comportamentos correspondem a ações possíveis de se acionar a par
tir de um objeto, buscando a produção de algum resultado específico.
Uma classe é um template para a geração de objetos. Por template deve-se ent
ender como uma estrutura, um “esqueleto”, que define quais atributos e comp
ortamentos serão possíveis para um determinado objeto. Em termos práticos, u
m objeto é dito como sendo uma instância (ou referência) de uma classe.
Nota do DevMan
Web Services são estruturas (serviços) que expõem funcionalidades de negóci
o como métodos que poderão ser consumidos por outras aplicações. A comuni
cação entre um Web Service e um software que dependa do mesmo se faz ger
almente via requisições HTTP. Devido a estas características, Web Services sã
o um dos principais recursos utilizados na integração entre diferentes sistemas
. Dentro da plataforma .NET, a tecnologia WCF (Windows Communication F
oundation) representa o principal meio para a construção de soluções baseadas
em serviços.
Serialização no .NET Framework:
uma visão geral
Serialização é o processo em que se converte o estado de objeto de um modo
que o mesmo possa ser persistido ou ainda, que facilite o seu transporte ao
longo de um meio de transmissão como uma rede (a Internet, por exemplo). A
desserialização é um tipo de operação em que acontece justamente o inverso,
ou seja, a partir do resultado de algo que havia sido serializado obtém-se uma
nova instância, exatamente igual à referência empregada inicialmente.
Por estado de um objeto devem ser consideradas informações presentes no
mesmo e normalmente vinculadas às suas propriedades (ou campos). Já a
noção de persistência refere-se a um procedimento no qual se gravam
informações em estruturas como arquivos, com tais dados podendo ser obtidos
novamente num momento posterior, basicamente através de operações de
leitura executadas a partir do local em que estes itens foram armazenados.
O .NET Framework conta com os seguintes tipos de serialização:
• Serialização em formato binário: possibilita que o estado de propriedades
(desconsideradas aquelas marcadas como somente leitura) e campos (variáveis
de instância) de um objeto sejam preservados em meios como um arquivo em
disco, um stream em memória, a partir da transmissão em rede de uma
aplicação .NET para outra etc. Membros públicos, protegidos ou até mesmo
privados são previstos dentro deste tipo de processo de serialização. Este
processo é realizado através de instâncias da classe BinaryFormatter
(namespace [Link]);
• Serialização em XML: apenas propriedades (executando aquelas definidas
como somente leitura) e campos públicos de uma classe são considerados
neste procedimento. Os mesmos meios de armazenamento disponíveis para a
serialização binária também são válidos para este segundo caso. O
procedimento que envolve o ato de serializar/desserializar um objeto em XML
acontece por meio de referências geradas a partir do tipo XmlSerializer
(declarado no namespace [Link]);
• Serialização no formato SOAP: neste tipo de técnica instâncias de objetos
são formatadas seguindo a especificação SOAP, a qual representa o padrão de
transmissão de dados mais utilizado por Web Services. Referências do tipo
SoapFormatter (namespace [Link])
são utilizadas para efetuar operações baseadas na definição SOAP;
• Serialização customizada: implementada através de classes baseadas na
interface ISerializable (estrutura pertencente ao namespace
[Link]), a qual conta com operações que permitem a
implementação de comportamentos específicos a serem definidos para
atividades de serialização/desserialização.
Nota do DevMan
Stream é uma sequência de dados de qualquer tamanho. Este tipo de estrutura
representa a base a partir da qual a plataforma .NET possibilita a manipulação
de arquivos e outros agrupamentos de informações, através de tipos envolvido
s em operações de escrita e leitura. As diferentes classes e mecanismos para tr
atamento de streams encontram-se disponíveis no namespace [Link].
XML é um formato baseado em marcações (tags), sendo considerada uma esp
écie de metalinguagem. O mesmo pode ser entendido como um conjunto de re
gras para a criação de uma nova linguagem: não existem tags pré-definidas, co
m as marcações sendo estipuladas conforme uma demanda específica.
Documentos em XML são formados por um conjunto de elementos (nodes), c
om as relações de dependência sendo estabelecidas por meio de níveis de hier
arquia. Um elemento, por sua vez, é constituído por uma tag e seu respectivo c
onteúdo. Quanto ao conteúdo, um elemento pode estar vazio, possuir atributos
ou mesmo, conter um agrupamento de outros elementos. Já um atributo é form
ado por um par constituído de um nome e um valor correspondente (entre aspa
s), sendo que estes dois itens encontram-se separados por um sinal de “=” (“ig
ual”) e são declarados dentro da tag que define um elemento.
Para uma classe passar pelo processo de serialização a mesma precisará estar
marcada com o atributo SerializableAttribute (presente no namespace System
do .NET), além de contar obrigatoriamente com um construtor público e sem
parâmetros (já que a desserialização precisará invocar este tipo de construção
para gerar uma instância). É possível ainda controlar se um elemento que
compõem um tipo constará ou não no resultado de uma instância que foi
serializada:
• No caso da serialização no formato binário, utiliza-se o atributo
NonSerializedAttribute sobre um campo que se deseje ignorar;
• Já para tarefas que envolvam a serialização em XML, o atributo XmlIgnore
(localizado no namespace [Link]) será o meio através do
qual se definirá que um campo ou propriedade deverá ser ignorado.
Nota do DevMan
SOAP ("Simple Object Access Protocol") é um formato que se baseia em XM
L, sendo comumente empregado em Web Services para a transmissão de infor
mações. O fato deste padrão ter sido constituído a partir da especificação XM
L permite que sistemas nas mais diferentes plataformas possam ser integrados
(desde que suportem o uso de Web Services empregando SOAP).
Nota do DevMan
Interface é um tipo de construção que define um conjunto de funcionalidades,
sendo normalmente implementada por uma ou mais classes. Declarações de m
étodos, propriedades e/ou eventos podem fazer parte de interfaces.
Uma interface corresponde, em termos conceituais, a uma espécie de "contrat
o" ou "esqueleto", que serve de base para a construção de uma ou mais classes
. Uma classe pode implementar ao mesmo tempo mais de uma interface (a her
ança de múltiplas classes, contudo, não é possível dentro da plataforma .NET)
.
Vantagens e desvantagens da
Serialização em .NET
Quando se considera o armazenamento de informações sem envolver o uso de
bancos de dados, a serialização pode se revelar como uma excelente
alternativa para cumprir este tipo de tarefa. Através dos mecanismos
disponíveis na plataforma .NET é possível realizar facilmente a serialização
de uma instância de objeto, economizando-se tempo e esforços do
desenvolvedor nestas operações.
Contudo, uma ressalva precisa ser feita para atividades de serialização como
um todo. Ações deste tipo costumam não contar com uma grande
performance, sendo a lentidão mais perceptível sobretudo quando se estiver
manipulando um objeto com uma estrutura complexa e que, portanto, englobe
um grande conjunto de informações.
A serialização binária costuma consumir menos espaço em disco ou memória
do que uma instância que tenha sido convertida para XML. No entanto, o
formato binário gera arquivos cujo conteúdo é de difícil compreensão, o que
não acontece quando se emprega a serialização em XML: a leitura de um
arquivo XML referente a um objeto serializado é simples, com as diferentes
propriedades da instância considerada estando normalmente separadas por
tags, acompanhadas de seus respectivos valores.
Podem ser citadas como outras vantagens da serialização em XML:
• A possibilidade de customizar os elementos que serão serializados por meio
de atributos, determinando nomes diferentes para campos e propriedades,
além de se definir que os mesmos serão convertidos como atributos ou
elementos dentro de um arquivo;
• Sendo XML um padrão aberto e de grande aceitação, sistemas
desenvolvidos em outras plataformas como Java podem consumir informações
geradas a partir da serialização XML realizada em um sistema em .NET.
No caso específico de objetos serializados em XML, uma desvantagem desta
técnica está no fato de um arquivo gerado a partir da mesma poder ser alterado
através de um editor de texto. Isto pode ser encarado em muitas situações
como um problema de segurança, já que abrem-se brechas para alterações no
conteúdo de um objeto, com tal ação podendo conduzir a erros durante uma
atividade que envolva uma operação de desserialização.
Exemplo de uso de Serialização em
.NET
A aplicação apresentada neste artigo foi implementada no .NET Framework
4.0, por meio da utilização do Microsoft Visual Studio 2010 Ultimate Edition,
sendo que o conteúdo da mesma poderá ser baixado a partir do site da revista.
O exemplo demonstrado a seguir tem por finalidade abordar a utilização dos
recursos de serialização da plataforma .NET, expondo algumas maneiras de
como empregar estes mecanismos na manipulação de instâncias de objetos.
Será construída uma aplicação em Windows Forms para a consulta ao extrato
de contas bancárias. Informações relativas a movimentações financeiras
estarão vinculadas a instâncias de uma classe, com objetos deste tipo podendo
ser serializados no formato binário ou em XML. Posteriormente, será possível
abrir os arquivos gerados, de maneira que através deste processo se consiga
visualizar em tela o extrato correspondente a uma conta.
Windows Forms é uma tecnologia do .NET Framework presente desde as
primeiras versões da plataforma, sendo um dos principais meios para o
desenvolvimento de aplicações dotadas de interfaces gráficas. Estas são
acionadas através da execução de programas executados a partir do ambiente
Windows.
O Windows Forms corresponde, basicamente, à continuação na plataforma
.NET do conjunto de padrões e recursos que eram empregados na construção
de aplicações visuais por meio do Visual Basic clássico e de outras
ferramentas como Delphi. O desenvolvimento nesta tecnologia gira em torno
da construção de formulários (janelas), as quais herdam o tipo básico Form
(namespace [Link]).
Dentro do Visual Studio existe um excelente suporte para a elaboração de
aplicações baseadas em Windows Forms. Esta IDE oferece uma ampla gama
de controles visuais como botões, caixas de texto/seleção, grids etc. Tais
componentes sendo combinados e inseridos em formulários, de forma a criar
aplicações com interfaces leves, com um aspecto visual satisfatório e que
atendam aos mais variados tipos de demanda.
Para criar o projeto de testes será necessário, dentro do Visual Studio, acessar
o menu File, opção New, sub opção Project. Dentro da tela New Project
(Figura 1) selecionar o template Windows Forms Application, preenchendo o
campo Name com o nome da aplicação a ser gerada (“TesteSerialization”,
neste caso); no campo Location é possível ainda definir o diretório no qual
serão criados os arquivos para este projeto.
Figura 1. Criando um projeto Windows Forms Application para testes
Criando classes contendo
informações de contas bancárias
Com a aplicação TesteSerialization, será necessário prosseguir com a
implementação das classes a serem utilizadas na manipulação de informações
sobre contas bancárias. Primeiramente será implementada a classe
MovimentacaoFinanceira. Para isto deve-se clicar, dentro do Solution
Explorer, com o botão direito do mouse sobre o projeto TesteSerialization,
escolhendo-se em seguida no menu de atalho a opção Add, sub opção New
Item. Neste momento será exibida a tela Add New Item (Figura 2); preencher
o campo Name com “[Link]”, a fim de gerar o arquivo
para implementação do tipo MovimentacaoFinanceira.
Figura 2. Criando o arquivo para implementação da classe
MovimentacaoFinanceira
Na Listagem 1 está a definição da classe MovimentacaoFinanceira, a qual
conterá informações como a data da movimentação financeira (propriedade
DataLancamento), a descrição da operação (propriedade
DescricaoMovimentacao) e o valor correspondente (propriedade
ValorMovimentacao). Importante destacar que o tipo
MovimentacaoFinanceira foi marcado com o atributo SerializableAttribute,
sendo esta uma instrução que indica que a classe em questão poderá ter seu
conteúdo serializado.
No caso específico da propriedade ValorMovimentacao, a mesma poderá
contar tanto com valores positivos (crédito), quanto negativos (débito). Já as
propriedades ValorDebito e ValorCredito são apenas para leitura, tendo sido
criadas para a apresentação de resultados em tela de uma maneira similar
àquela encontrada num extrato bancário.
ValorDebito e ValorCredito são também exemplos de utilização do recurso
conhecido como Nullable. Neste tipo de construção uma propriedade pode
conter valores null, além de dados convencionais baseados em tipos
primitivos (como decimal, int, double etc.).
Independente de a serialização acontecer no formato binário ou em XML,
apenas as propriedades DataLancamento, DescricaoMovimentacao e
ValorMovimentacao serão persistidas em instâncias que constem no arquivo
gerado como resultado deste processo. As propriedades ValorDebito e
ValorCredito serão desconsideradas, já que foram definidas como somente
leitura.
Listagem 1. Classe MovimentacaoFinanceira
using System;
using [Link];
using [Link];
using [Link];
namespace TesteSerialization
{
[Serializable]
public class MovimentacaoFinanceira
{
public DateTime DataLancamento { get; set; }
public string DescricaoMovimentacao { get; set; }
public double ValorMovimentacao { get; set; }
public double? ValorDebito
{
get
{
if (ValorMovimentacao < 0)
return ValorMovimentacao;
else
return null;
}
}
public double? ValorCredito
{
get
{
if (!(ValorMovimentacao < 0))
return ValorMovimentacao;
else
return null;
}
}
}
}
Nota do DevMan
Uma construção cujo tipo é Nullable corresponde a uma instância baseada na
estrutura Nullable<T>, em que T pode representar qualquer espécie de dado p
rimitivo (int, bool, float, double etc.) ou ainda, valores baseados em tipos com
o DateTime (ou outras structs similares a esta, por exemplo). Graças a essa ins
trução, é possível associar um valor null a elementos (propriedades ou campos
de uma classe) que originalmente estariam vinculados a um valor default (zero
no caso de tipos numéricos).
Um tipo struct é um recurso normalmente utilizado para agrupar pequenos con
juntos de variáveis, os quais geralmente contam com informações relacionada
s. Um exemplo seria a criação de um struct para conter as coordenadas de um
quadrilátero (figura geométrica com quatro lados).
Já a Listagem 2 apresenta a implementação da classe ContaBancaria. Este
tipo é formado por propriedades que servem de identificação de uma conta
corrente (CPF, nome do correntista, banco, agência, código da conta corrente),
além das diferentes movimentações financeiras realizadas pelo cliente em
questão (propriedade Movimentacoes).
Assim como aconteceu com MovimentacaoFinanceira, a classe ContaBancaria
também foi marcada com o atributo SerializableAttribute, a fim de permitir
que essa estrutura possa ser persistida. O fato de todas as propriedades
permitirem o acesso para leitura e escrita resultará na serialização de todos
estes elementos.
Importante ressaltar o fato da propriedade Movimentacoes ser uma lista de
objetos baseada no tipo List<MovimentacaoFinanceira>, com este último
representando uma implementação do mecanismo conhecido como Generics.
Nota do DevMan
Por convenção, os nomes de classes que representam atributos são finalizados
com o sufixo Attribute. Expressões que envolvam um atributo vinculado a um
a estrutura de código dispensam o uso de tal sufixo. Logo, ao se utilizar o atrib
uto SerializableAttribute emprega-se apenas “Serializable” na construção a ser
marcada.
Listagem 2. Classe ContaBancaria
using System;
using [Link];
using [Link];
using [Link];
namespace TesteSerialization
{
[Serializable]
public class ContaBancaria
{
public string CPFCorrentista { get; set; }
public string NomeCorrentista { get; set; }
public string NomeBanco { get; set; }
public string Agencia { get; set; }
public string ContaCorrente { get; set; }
public List<MovimentacaoFinanceira> Movimentacoes { get;
set; }
}
}
Nota do DevMan
Generics são estruturas como classes, interfaces e métodos que podem aceitar
um ou mais tipos como parâmetro. No caso uma lista baseada em List<T>, T
pode ser qual tipo concreto ou primitivo. Um exemplo de uso seria uma lista d
e strings, em que se empregaria o tipo List<string> para conter instâncias que
representem trechos de textos. O grande benefício da utilização de Generics es
tá no fato de que com este recurso se evitam implementações específicas para
certas construções, reaproveitando assim todo um conjunto de funcionalidades
que já foi definido anteriormente.
O tipo estático ArquivoContaBinario (Listagem 3) será empregado,
basicamente, no processo de serialização e geração de arquivos binários a
partir de referências de ContaBancaria. Esta classe conta com os seguintes
métodos:
• Serializar: operação que a partir de um nome de arquivo e um objeto
ContaBancaria cria um arquivo correspondente no formato binário;
• Deserializar: recebendo como parâmetro o nome de um arquivo binário já
existente, esta operação devolverá uma instância do tipo ContaBancaria.
O funcionamento do método Serializar envolve os seguintes passos:
• Inicialmente é gerado um objeto do tipo FileStream, com o resultado sendo
atribuído à variável de nome stream. Conforme já citado, Streams são a base
para a manipulação de arquivos e outras estruturas de armazenamento em
.NET. O construtor de FileStream recebe como parâmetros o nome do
arquivo, uma indicação de que se estará gerando um novo arquivo
([Link]) e que o acesso a este será para escrita ([Link]).
Tudo isto acontece dentro de uma cláusula using, sendo que essa instrução
garante que, na eventualidade de qualquer erro ocorrer ao longo do código que
constar no seu interior, o Stream será fechado;
• Uma nova instância baseada na classe BinaryFormatter é então criada, sendo
que a invocação do método Serialize permite salvar o objeto ContaBancaria
(parâmetro conta) no Stream gerado anteriormente e que representa o arquivo
que se está montando (variável stream);
• Por fim é acionado o método Close da variável stream, a fim de concluir o
procedimento de geração do arquivo binário com a instância do tipo
ContaBancaria serializada.
O método Deserializar opera de maneira bastante semelhante a Serializar,
contando apenas com as seguintes diferenças:
• A instância de FileStream criada para o arquivo a ser manipulado recebe
como parâmetros instruções que indicam que essa estrutura será aberta
([Link]), assim como que o acesso será de leitura ([Link]);
• O método Deserialize do objeto BinaryFormatter gerado para este processo
converterá o conteúdo que se encontra no arquivo binário para uma instância
de ContaBancaria, a qual será devolvida ao final da execução da operação
Desserializar.
FileStream é uma classe do .NET que está definida no namespace [Link],
representando a base para a manipulação de arquivos dentro da plataforma
.NET. A partir do tipo FileStream torna-se possível a realização de operações
criando novos arquivos, lendo o conteúdo de estruturas já existentes ou
mesmo, alterando ou gravando novas informações.
A classe FileStream conta com diversos construtores. Uma das versões mais
utilizadas para a geração de instâncias de FileStream recebe os seguintes
parâmetros:
Nome do arquivo que estará sendo manipulado/gerado;
Uma instrução que especifica o modo como o arquivo em questão
estará sendo utilizado, baseando-se para isto no enumeration FileMode.
O valor [Link] permite criar um novo arquivo, descartando
uma versão anterior caso essa exista no diretório de destino;
[Link] abre um arquivo pré-existente; já
[Link] abre o arquivo considerado se o mesmo
existir, em caso negativo essa instrução orientará a instância de
FileStream a realizar os procedimentos necessários para a criação de
um novo arquivo;
Um último parâmetro que indica o modo de acesso ao arquivo,
dependendo neste caso do enumeration FileAccess. O valor
[Link] está associado a operações de leitura, já
[Link] é o equivalente para o processo de escrita de
informações em um arquivo; [Link] indica que a
instância de FileStream é empregada tanto em operações de leitura
quanto escrita.
Alguns dos métodos mais comumente utilizados com objetos do tipo
FileStream são:
Read: realiza a leitura de blocos de dados de um arquivo, sendo
informada a posição inicial e final dos bytes desejados;
Write: operação que efetua a gravação de blocos de dados em um
arquivo;
Lock: cria um bloqueio no arquivo que está sendo manipulado por uma
instância de FileStream, de maneira que outros processos não possam
acessar o arquivo considerado para a realização de operações de leitura
e/ou escrita;
Seek: permite a navegação até um determinado ponto de um arquivo,
de onde poderão ser realizadas tarefas de leitura ou escrita. Por default,
ao ser aberto ou criado um arquivo, um FileStream estará posicionado
no início desta estrutura;
Close: fecha o arquivo em questão, liberando os recursos que haviam
sido alocados para a manipulação do mesmo.
A propriedade Length também é outro elemento bastante utilizado em
implementações que envolvam a utilização da classe FileStream, retornado o
tamanho em bytes do arquivo em uso.
Nota do DevMan
Um construtor é um método especial que permite a criação de instâncias de u
ma classe. Este tipo de construção leva o mesmo nome da classe ao qual se ref
ere, não devolvendo nenhum tipo de dado e pode conter um ou mais parâmetr
os ou ainda, nenhum elemento deste tipo. É costume que dentro de um constru
tor seja executada uma série de ações com a finalidade de inicializar o estado
de um objeto. Quando se define uma classe em .NET e nenhum construtor é d
eclarado dentro da mesma, o compilador da plataforma assume que a classe e
m questão poderá ser acessada, normalmente através de um construtor sem par
âmetros.
Já o BinaryFormatter é uma classe que permite a realização de operações de
serialização e desserialização de objetos, empregando normalmente arquivos
num formato binário específico da plataforma .NET. O tipo BinaryFormatter
sempre parte do pressuposto que a classe de um objeto que passou ou que
estará sujeita a operações de serialização/desserialização tenha sido marcada
com o atributo SerializableAttribute.
O uso de BinaryFormatter normalmente gira em torno das operações Serialize
e Deserialize. O primeiro destes métodos (Serialize) é geralmente utilizado
para a gravação dos dados contidos em um objeto num arquivo binário. Já o
segundo método (Deserialize) realiza o procedimento inverso, ou seja, a partir
das informações presentes em um arquivo no formato binário gera-se uma
nova instância idêntica ao objeto que foi serializado num primeiro momento.
Listagem 3. Classe ArquivoContaBinario
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
namespace TesteSerialization
{
public static class ArquivoContaBinario
{
public static void Serializar(
string nomeArquivo, ContaBancaria conta)
{
using (FileStream stream = new FileStream(nomeArquivo,
[Link], [Link]))
{
BinaryFormatter formatter = new BinaryFormatter();
[Link](stream, conta);
[Link]();
}
}
public static ContaBancaria Deserializar(
string nomeArquivo)
{
ContaBancaria conta;
using (FileStream stream = new FileStream(nomeArquivo,
[Link], [Link]))
{
BinaryFormatter formatter = new BinaryFormatter();
conta =
(ContaBancaria)[Link](stream);
[Link]();
}
return conta;
}
}
}
ArquivoContaXML (Listagem 4) é uma classe estática bastante semelhante
ao tipo ArquivoContaBinario, apenas diferindo nos seguintes aspectos:
• O uso da classe BinaryFormatter foi substituído por instâncias do tipo
XmlSerializer nos métodos Serializar e Deserializar;
• O construtor de XmlSerializer precisa de uma referência que contenha
definições do tipo a ser empregado em operações de
serialização/desserialização, por meio do uso da expressão typeof em conjunto
com a classe ContaBancaria.
O operador typeof é utilizado para obter informações sobre uma classe, tipo
de dado ou interface. Instruções que façam uso deste operador fornecem ao
mesmo o nome da estrutura a ser analisada, com a expressão em questão
devolvendo uma referência da classe Type. A partir desta instância da classe
Type é possível obter dados detalhados de métodos, construtores,
propriedades e outros elementos que compõem a estrutura (tipo, classe ou
interface) informada à instrução typeof.
O XmlSerializer é uma classe similar a BinaryFormatter, sendo também
utilizada em operações de serialização e desserialização de objetos. A única
diferença, neste caso, é que as operações definidas em XmlSerializer são
executadas a partir de um documento XML, ao contrário de BinaryFormatter
que se baseia em informações no formato biná[Link] de dados gravados
no padrão binário, as informações de objetos serializados no formato XML
são legíveis, podendo tranquilamente visualizar as mesmas a partir da abertura
do documento em questão em um editor de texto. Apenas propriedades e
atributos públicos de uma classe são serializados (ao contrário do processo
binário que persiste qualquer estrutura destes tipos). Quando se considera o
procedimento de conversão de uma instância de objeto para XML,
propriedades são gravadas por default como elementos do documento; os
dados contidos nestas propriedades representam valores entre as chaves que
correspondem a tais [Link] como também acontece com
BinaryFormatter, o tipo XmlSerializer efetua ações de serialização e
desserialização por meio dos métodos Serialize e Deserialize,
respectivamente.
Listagem 4. Classe ArquivoContaXML
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
namespace TesteSerialization
{
public static class ArquivoContaXML
{
public static void Serializar(
string nomeArquivo, ContaBancaria conta)
{
using (FileStream stream = new FileStream(nomeArquivo,
[Link], [Link]))
{
XmlSerializer serializer =
new XmlSerializer(typeof(ContaBancaria));
[Link](stream, conta);
[Link]();
}
}
public static ContaBancaria Deserializar(
string nomeArquivo)
{
ContaBancaria conta;
using (FileStream stream = new FileStream(nomeArquivo,
[Link], [Link]))
{
XmlSerializer serializer =
new XmlSerializer(typeof(ContaBancaria));
conta =
(ContaBancaria)[Link](stream);
[Link]();
}
return conta;
}
}
}
Nota do DevMan
Uma classe estática é um tipo que não pode ser instanciado, ou seja, por defini
ção impede-se que instâncias de objetos sejam geradas a partir dessa estrutura.
Métodos disponibilizados por este tipo de construção também são estáticos, co
m tais operações estando normalmente voltadas à realização de tarefas que nã
o envolvam o armazenamento de informações de estado (e que estariam vincu
ladas necessariamente a um objeto da classe em questão).
SimulacaoExtratos (Listagem 5) é um tipo estático que será empregado para
simulações, sendo composto pelas operações:
• GerarMovimentacao: utilizado na geração de instâncias do tipo
MovimentacaoFinanceira;
• ObterDadosContaA: método que conta com instruções para criar e retornar
uma instância de ContaBancaria;
• ObterDadosContaB: método bastante similar a ObterDadosContaA,
responsável por criar e devolver uma instância que representa uma outra
conta;
ObterDadosContaA e ObterDadosContaB fazem uso da operação
GerarMovimentacao, a fim de instanciar objetos relativos a movimentações
financeiras ligadas a uma conta. Além disso, em ambos os casos, referências
de List<MovimentacaoFinanceira> são também criadas e atribuídas à
propriedade Movimentacoes de um objeto ContaBancaria, de maneira que esta
propriedade armazene os diferentes lançamentos envolvendo uma conta
corrente.
Listagem 5. Classe SimulacaoExtratos
using System;
using [Link];
using [Link];
using [Link];
namespace TesteSerialization
{
public static class SimulacaoExtratos
{
private static MovimentacaoFinanceira GerarMovimentacao(
DateTime dtLancamento,
string dsMovimentacao,
double vlMovimentacao)
{
MovimentacaoFinanceira movimentacao =
new MovimentacaoFinanceira();
[Link] = dtLancamento;
[Link] = dsMovimentacao;
[Link] = vlMovimentacao;
return movimentacao;
}
public static ContaBancaria ObterDadosContaA()
{
ContaBancaria conta = new ContaBancaria();
[Link] = "222.222.222-22";
[Link] = "JOÃO DA SILVA";
[Link] = "BANCO DO PROGRESSO";
[Link] = "0365";
[Link] = "4144-1";
[Link] =
new List<MovimentacaoFinanceira>();
[Link](GerarMovimentacao(
new DateTime(2012, 02, 06),
"Saldo Inicial", 500.67));
[Link](GerarMovimentacao(
new DateTime(2012, 02, 08),
"Taxa Cartão de Crédito", -30.00));
[Link](GerarMovimentacao(
new DateTime(2012, 02, 08),
"IOF", -15.75));
[Link](GerarMovimentacao(
new DateTime(2012, 02, 08),
"Salário", 6900.37));
return conta;
}
public static ContaBancaria ObterDadosContaB()
{
ContaBancaria conta = new ContaBancaria();
[Link] = "333.333.333-33";
[Link] = "MARIA DE OLIVEIRA";
[Link] = "BANCO DO DESENVOLVIMENTO NACIONAL";
[Link] = "0463";
[Link] = "7456-3";
[Link] =
new List<MovimentacaoFinanceira>();
[Link](GerarMovimentacao(
new DateTime(2012, 02, 06),
"Saldo Inicial", 1237.43));
[Link](GerarMovimentacao(
new DateTime(2012, 02, 08),
"Taxa Uso Conta Bancária", -25.00));
[Link](GerarMovimentacao(
new DateTime(2012, 02, 08),
"Prestação de Serviços", 7854.54));
return conta;
}
}
}
Implementação das telas para
testes
Agora é necessário criar o formulário para a consulta às diferentes
movimentações de um correntista. Sobre o projeto TesteSerialization, clique
com o botão direito do mouse e selecione Add > New Item. Em seguida
adicione um novo formulário; o campo Name deve ser preenchido como
“[Link]”.
A Figura 3 demonstra um exemplo, em termos gerais, de como poderá ficar
tal formulário ao fim dos procedimentos mencionados nesta seção.
Figura 3. Formulário de consulta em modo Design com todos os controles já
configurados
Alterar, por meio da janela Properties do Visual Studio, a propriedade Text de
FormConsultaAgenda para “Teste de recursos de Serialização em .NET -
Consulta a Informações de Contas”.
Nota: serão omitidos deste artigo, para efeitos de simplificação, os valores de
propriedades de alguns controles.
Adicione ao formulário dois controles do tipo Button (parte superior da janela,
com um botão ao lado outro), além de outros dois controles do tipo GroupBox
(o primeiro logo abaixo dos botões, com o segundo na sequência a este).
A Tabela 1 lista os conteúdos das propriedades Name e Text para cada
controle.
Tipo de Controle Propriedade Name Propriedade Text
Button btnAbrirArquivo Abrir Arquivo
Button btnSair Sair
GroupBox grpDadosConta Dados da Conta
GroupBox grpMovimentacoes Movimentações
Tabela 1. Propriedades a serem preenchidas para controles dentro de
FormConsultaConta
No interior do controle grpDadosConta deverão ser adicionados os controles
do tipo Label e TextBox listados na Tabela 2. Dentro de Tabela 2 já estão
definidos também os respectivos valores para as propriedades Name e Text de
cada componente Label, além do controle TextBox relacionado a cada um
destes itens.
Propriedade Name do Propriedade Text do Propriedade Name do
componente Label componente Label TextBox equivalente
lblCPFCorrentista CPF do Correntista txtCPFCorrentista
lblNomeCorrentista Nome do Correntista txtNomeCorrentista
lblNomeBanco Banco txtNomeBanco
lblAgencia Agência txtAgencia
lblContaCorrente Conta Corrente txtContaCorrente
lblVlSaldoConta Saldo txtVlSaldoConta
Tabela 2. Componentes Label e TextBox que estarão dentro de
grpDadosConta
Já dentro do GroupBox grpMovimentacoes (que teve sua propriedade Text
configurada para “Movimentações”), será necessário adicionar um controle
DataGridView cuja propriedade Name será grdMovimentacoes. Este controle
terá sua propriedade ReadOnly preenchida como “true”.
O objeto DataGridView é um controle visual que pode ser empregado em
aplicações Windows Forms para a apresentação de dados num formato tabular
(grid). Esta forma de representar informações é similar àquela utilizada pelo
Microsoft Excel, ou seja, diferentes dados são dispostos ao longo de uma
tabela composta por colunas e linhas. À posição que corresponde a uma
coluna de uma determinada linha dá-se o nome de célula.
Informações podem ser exibidas apenas para efeitos de consulta em um
DataGridView, embora também seja possível editar ou incluir dados através
da digitação destes valores em células da grid. Os dados associados à
propriedade DataSource de um DataGridView são a base para a exibição de
informações por meio destes controles.
Por fim, incluir no formulário um componente OpenFileDialog (disponível na
seção Dialogs da Toolbox do Visual Studio), preenchendo as propriedades
Name com “openFileDialogDadosContas” e Filter com “Arquivo binário
(*.dat)|*.dat|Arquivo XML (*.xml)|*.xml” (em ambos os casos retirar as aspas
no preenchimento destas configurações). Já a propriedade FileName deverá
estar em branco.
Arquivos no formato .dat contêm normalmente dados que são consumidos por
aplicações de software. Este tipo de arquivo pode ser formado tanto por
informações em modo texto quanto num formato binário. Não existe um
padrão que determine como os dados devam ser dispostos em um arquivo de
extensão .dat: cada aplicação conta com seus próprios padrões específicos
para o armazenamento de informações em estruturas deste tipo.
O controle openFileDialogDadosContas será utilizado para selecionar
arquivos contendo instâncias de ContaBancaria que foram serializadas
anteriormente, a fim de recuperar as informações destas e apresentar isto em
tela.
Uma vez que as propriedades visuais do formulário tenham sido devidamente
configuradas, deve-se proceder com a implementação do código-fonte para a
consulta a informações presentes em arquivos binários ou XML.
Com o arquivo [Link] no Modo Design no Visual Studio,
selecione o botão btnAbrirArquivo e efetue um duplo clique sobre o mesmo:
este procedimento irá gerar o método btnAbrirArquivo_Click (Listagem 6), o
qual equivale à implementação do evento Click para este controle (com o
mesmo sendo acionado em tempo de execução quando o usuário clicar em
btnAbrirArquivo).
Outra forma de fazer isso com o botão btnAbrirArquivo marcado, seria
acessar o botão Events da janela Properties (o mesmo possui como ícone o
desenho de um “raio”); a partir disto, selecionar o evento Click e no campo
para digitação que se abre efetuar um duplo clique.
Eventos são métodos executados dentro de uma aplicação em resposta a
alguma ação iniciada por um usuário ou até mesmo, como resultado do
processamento de uma sequência de operações pelo próprio software em
questão. Em torno do conceito de evento foi concebido o paradigma
conhecido como programação orientada a eventos, com a construção de
aplicações baseadas em interfaces gráficas utilizando em larga escala as ideias
que formam este padrão.
Quando se considera o desenvolvimento de aplicações em Windows Forms no
.NET, alguns eventos bastante conhecidos e normalmente implementados por
desenvolvedores são:
Click: normalmente associado a cliques do mouse em controles como
Button;
Shown: acionado quando um formulário é apresentado em tela;
Closed: disparado quando um usuário solicita o fechamento de uma
janela, sendo que isto acontece através do acionamento de alguma
funcionalidade específica ou ainda, clicando-se no botão "Fechar" da
tela considerada.
Sobre o método btnAbrirArquivo_Click é importante destacar os seguintes
pontos:
• Com o objetivo de exibir a tela em que um usuário poderá selecionar um
arquivo binário (extensão .dat) ou XML, aciona-se o método ShowDialog do
controle openFileDialogDadosContas. Caso o mesmo retorne o valor
[Link] (indicação de que um arquivo foi selecionado pelo usuário),
a tarefa de abertura de um arquivo serializado prossegue;
• A propriedade FileName de openFileDialogDadosContas retorna o nome do
arquivo escolhido por um usuário;
• Se o arquivo tiver a extensão .dat (checagem efetuada através do método
EndsWith de String), emprega-se o método Deserializar da classe
ArquivoContaBinario, visando a obtenção da instância correspondente para o
tipo ContaBancaria;
• Caso se trate de um arquivo XML, utiliza-se então a operação Deserializar
definida no tipo ArquivoContaXML para recuperar a instância de
ContaBancaria serializada no documento em questão;
• Por fim, os diferentes controles do formulário tem seu conteúdo preenchido
com os valores presentes na instância de nome conta. Especificamente para o
TextBox txtVlSaldoConta, emprega-se uma expressão lambda com o método
Sum, de maneira que se calcule o saldo total da conta bancária considerada.
Nota do DevMan
OpenDialog é um controle visual do Windows Forms que permite a exibição d
e uma janela a partir da qual um usuário pode selecionar arquivos. Trata-se do
mesmo tipo de funcionalidade representado pela opção Abrir, em produtos co
mo o MS Word ou Excel. O método ShowDialog disponível em um objeto Op
enDialog permite a exibição da tela para seleção de arquivos, sendo que é poss
ível inclusive filtrar quais os tipos de arquivos poderão estar sendo selecionad
os.
Lambda expression é um tipo de função anônima, ou seja, uma estrutura que c
onta com um corpo (formado por uma ou mais expressões), mas que não apres
enta um nome e uma organização similar à de métodos convencionais. Trata-s
e de um recurso bastante utilizado como meio para a passagem de parâmetros
a outras operações. Normalmente uma lambda expression é formada por uma i
nstrução curta e, em muitos casos, não excedendo uma linha de código, fato es
te que representa uma interessante vantagem ao evitar que numerosas funções
simples precisem ser implementadas ao longo de uma classe.
Listagem 6. Evento btnAbrirArquivo_Click
...
private void btnAbrirArquivo_Click(object sender, EventArgs e)
{
if ([Link]() == [Link])
{
ContaBancaria conta = null;
string nomeArquivo = [Link];
if ([Link]().EndsWith(".dat"))
{
conta =
[Link](nomeArquivo);
}
else
{
conta =
[Link](nomeArquivo);
}
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link](
m => [Link]).ToString("0.00");
[Link] = [Link];
...
}
}...
Renomeie o arquivo [Link] para [Link]. Posteriormente, altere
por meio da janela Properties do Visual Studio a propriedade Text de
FormPrincipal para “Teste de recursos de Serialização em .NET”.
Existirão dentro de FormPrincipal os controles do tipo Button mencionados
na Tabela 3, na qual foram listados os conteúdos das propriedades Name e
Text para tais componentes.
Propriedade Name Propriedade Text
btnGerarArquivoBinario Gerar Arquivo Binário
btnGerarArquivoXML Gerar Arquivo XML
btnConsultarArquivo Consulta a Arquivo Serializado
btnSair Sair
Tabela 3. Controles visuais a serem configurados em FormPrincipal
Inclua em FormPrincipal dois componentes SaveFileDialog (controle este
presente na seção Dialogs da Toolbox do Visual Studio), conforme indicado
na Tabela 4. O primeiro destes será utilizado para a geração de um arquivo
com uma instância de ContaBancaria serializada no formato binário, ao passo
que o segundo estará envolvido com a operação equivalente para a produção
de um documento XML. Além de Name, a Tabela 4 lista ainda o valor para
preenchimento da propriedade Filter em cada um dos objetos SaveFileDialog
(de modo que estes exibam a extensão com a qual serão gerados arquivos a
partir dos mesmos).
Propriedade Name Propriedade Filter
saveFileDialogFormatoBinario Arquivo binário (*.dat)|*.dat
saveFileDialogFormatoXML Arquivo XML (*.xml)|*.xml
Tabela 4. Controles a serem configurados em FormPrincipal
Os seguintes métodos estão sendo implementados em FormPrincipal:
• btnGerarArquivoBinario_Click: evento Click do botão
btnGerarArquivoBinario, sendo acionado para a geração de um arquivo
binário de extensão .dat. Caso a operação ShowDialog de
saveFileDialogFormatoBinario retorne “true”, utiliza-se a classe
ArquivoContaBinario para serializar a instância retornada pelo método
ObterDadosContaA do tipo SimulacaoExtratos, exibindo-se ainda uma
mensagem de que o processo ocorreu normalmente ao final;
• btnGerarArquivoXML_Click: corresponde ao evento Click do componente
btnGerarArquivoXML. Possui funcionamento similar a
btnGerarArquivoBinario, diferindo apenas pelo uso do controle
saveFileDialogFormatoXML e do tipo estático ArquivoContaXML para a
criação de arquivos no formato XML.
• btnConsultarArquivo_Click: evento Click do controle btnConsultarArquivo.
Dentro do mesmo uma instância FormConsultaConta é criada, acionando-se
na sequência o método ShowDialog com o objetivo de exibir a tela para
consulta de informações sobre contas bancárias (as quais constam em arquivos
serializados anteriormente).
Listagem 7. Eventos a serem implementados em FormPrincipal
...
private void btnGerarArquivoBinario_Click(object sender, EventArgs
e)
{
if ([Link]() ==
[Link])
{
ContaBancaria contaA = [Link]();
[Link](
[Link],
contaA);
[Link]([Link](
"Arquivo {0} gerado com sucesso!",
[Link]));
}
}
private void btnGerarArquivoXML_Click(object sender, EventArgs e)
{
if ([Link]() == [Link])
{
ContaBancaria contaB = [Link]();
[Link](
[Link],
contaB);
[Link]([Link](
"Arquivo {0} gerado com sucesso!",
[Link]));
}
}
private void btnConsultarArquivo_Click(object sender, EventArgs e)
{
FormConsultaConta formConsulta = new FormConsultaConta();
[Link]();
}
...
A Figura 4 apresenta a estrutura do projeto TesteSerialization ao término da
implementação das classes que constituem o mesmo.
Figura 4. Estrutura do projeto TesteSerialization
Executando a aplicação de testes
A execução do projeto TesteSerialization pode ser feita, dentro do Visual
Studio, acessando o menu Debug, opção Start Debugging. Outra forma seria
utilizar a tecla de atalho F5 dentro da IDE do Visual Studio. Uma vez que esta
ação tenha sido disparada, será exibida a tela indicada pela Figura 5.
Debug/Debugging é uma técnica para análise de código-fonte em que um
desenvolvedor pode, a partir de uma IDE para construção de softwares,
executar trechos de uma aplicação passo a passo. A partir disto é possível
verificar se determinados pontos do código são realmente executados
conforme o esperado, checando ainda se os valores atribuídos a variáveis e
objetos são corretos. Sendo uma das mais poderosas ferramentas de
desenvolvimento hoje no mercado, o Visual Studio oferece um amplo suporte
para a aplicação de técnicas de debugging em soluções construídas através do
mesmo.
Figura 5. Tela inicial que é exibida ao se executar o projeto TesteSerialization
Clicando na opção “Gerar Arquivo Binário” será então apresentada a tela que
consta na Figura 6. Caso o usuário opte por concluir o processo confirmando
a geração de um arquivo de extensão binário de extensão .dat, o mesmo irá se
deparar com uma janela de aviso semelhante àquela demonstrada na Figura 7.
Figura 6. Tela para gravação de um arquivo binário
Figura 7. Confirmação de que um arquivo binário foi gerado
A Figura 8 exibe, a partir do Bloco de Notas, o arquivo binário que foi gerado
contendo informações de uma conta bancária.
Figura 8. Arquivo binário sendo visualizado no Bloco de Notas do Windows
Já na Figura 9 mostra a tela para geração de um arquivo serializado em XML.
Se o usuário escolher pela execução deste processo até o final, será exibida ao
mesmo a tela constante na Figura 10.
Figura 9. Tela para gravação de um arquivo XML
Figura 10. Confirmação de que um arquivo XML foi gerado
O arquivo XML gerado também poderá ser visualizado a partir do Internet
Explorer, conforme demonstrado na Figura 11.
Figura 11. Arquivo XML sendo visualizado no Internet Explorer
Conclusão
Mesmo aplicações simples e destinadas a usuários finais, podem depender de
alguma forma para armazenar informações, objetivando a recuperação de
certos dados num momento futuro. Dado o fato de tais programas serem
executados nos mais variados tipos de computadores, o uso de bancos de
dados relacionais pode se revelar como trabalhoso e até mesmo impeditivo,
devido muitas vezes a restrições de hardware e do ambiente em que tais
softwares são empregados.
Não é raro também que em tais situações desenvolvedores implementem
mecanismos específicos, visando a gravação e posterior leitura de informações
necessárias à operação das aplicações que estão sendo construídas. A técnica
conhecida como serialização procura evitar esforços deste tipo,
disponibilizando meios que permitam salvar o estado de instâncias de objetos
em estruturas como arquivos. Logo, conclui-se que este tipo de prática pode
contribuir não só para uma maior produtividade, assim como viabilizar que
objetos sejam salvos e possam ser recuperados num momento futuro.
O .NET Framework conta com um abrangente conjunto de recursos de
serialização, com mecanismos que suportam operações desse gênero em
formatos binários e até XML. Cada uma destas alternativas contam com suas
vantagens e desvantagens, cabendo aos programadores que venham a fazer
uso das mesmas analisar os prós e contras, a fim de selecionar a melhor opção
para um determinado contexto.
Links
FileStream Class[Link]
[Link]
Generics in the .NET Framework
[Link]
LINQ to Objects
[Link]
Nullable
[Link]
Serialization
[Link]
Static Classes and Static Class Members
[Link]
Struct
[Link]
por Renato Jose
Guru .net e tecnologias MS