340
Capítulo 1 – Tipos de dados e Interfaces 1 Israel Aece | http://www.projetando.net 1 Capitulo 1 Tipos de dados e Interfaces Introdução O .NET Framework 2.0 fornece uma porção de tipos predefinidos que são necessários para a criação dos mais diversos tipos de aplicações que o mesmo fornece para nós desenvolvedores. Esses tipos são utilizados constantemente nas aplicações, para armazenar valores, bem como uma estrutura extensível para que você crie seus próprios tipos. Neste capítulo você entenderá como funciona a arquitetura de tipos do .NET Framework 2.0, verá também sobre os tipos intrínsicos disponíveis. Além disso, vamos abordar um assunto fortemente ligado aos tipos: tipos-valor e tipos-referência e, para complementar, analisaremos o boxing e unboxing, que é um grande vilão em termos de performance das aplicações .NET-based. Para finalizar a parte sobre tipos, temos ainda alguns tipos especiais que precisam ser examinados: Generics, Nullable Types, Exceptions e Attributes. Na segunda parte do capítulo, vamos analisar as Interfaces que são disponíveis dentro do .NET Framework 2.0 e também veremos como criar suas próprias Interfaces para utilizar no seu código ou, se desejar, expor esta para que consumidores de seu componente consigam implementá-la, criando uma espécie de “contrato” que teus componentes exigem para poderem trabalhar. Examinando a arquitetura e os tipos CTS – Common Type System Como todos sabem, os tipos são peça fundamental em qualquer tipo de linguagem de programação, seja para a criação de componentes ou para tipos de aplicações que interajam com o usuário (Windows ou Web). Como a idéia da Microsoft é tornar tudo o mais integrado possível, ela criou uma especificação de tipos chamada Common Type System (CTS), que descreve como os tipos são definidos e como se comportam. Esses tipos são compartilhados entre todas as linguagens .NET. Sendo assim, você pode criar um componente em Visual C# e consumir este mesmo componente em uma aplicação cliente escrita em Visual Basic .NET sem nenhum problema, já que o tipo esperado ou o tipo a ser devolvido são de conhecimento de ambas. Você também ouvirá o termo cross-language integration para essa mesma explicação. Além disso, um outro ponto importante do CTS é que também específica as regras de visibilidade de tipos para acesso aos membros do mesmo. Atualmente temos os seguintes modificadores de acesso disponíveis:

Livro de .NET - Israel Aece

Embed Size (px)

Citation preview

Page 1: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 1

Israel Aece | http://www.projetando.net

1

Capitulo 1 Tipos de dados e Interfaces Introdução O .NET Framework 2.0 fornece uma porção de tipos predefinidos que são necessários para a criação dos mais diversos tipos de aplicações que o mesmo fornece para nós desenvolvedores. Esses tipos são utilizados constantemente nas aplicações, para armazenar valores, bem como uma estrutura extensível para que você crie seus próprios tipos. Neste capítulo você entenderá como funciona a arquitetura de tipos do .NET Framework 2.0, verá também sobre os tipos intrínsicos disponíveis. Além disso, vamos abordar um assunto fortemente ligado aos tipos: tipos-valor e tipos-referência e, para complementar, analisaremos o boxing e unboxing, que é um grande vilão em termos de performance das aplicações .NET-based. Para finalizar a parte sobre tipos, temos ainda alguns tipos especiais que precisam ser examinados: Generics, Nullable Types, Exceptions e Attributes. Na segunda parte do capítulo, vamos analisar as Interfaces que são disponíveis dentro do .NET Framework 2.0 e também veremos como criar suas próprias Interfaces para utilizar no seu código ou, se desejar, expor esta para que consumidores de seu componente consigam implementá-la, criando uma espécie de “contrato” que teus componentes exigem para poderem trabalhar. Examinando a arquitetura e os tipos CTS – Common Type System Como todos sabem, os tipos são peça fundamental em qualquer tipo de linguagem de programação, seja para a criação de componentes ou para tipos de aplicações que interajam com o usuário (Windows ou Web). Como a idéia da Microsoft é tornar tudo o mais integrado possível, ela criou uma especificação de tipos chamada Common Type System (CTS), que descreve como os tipos são definidos e como se comportam. Esses tipos são compartilhados entre todas as linguagens .NET. Sendo assim, você pode criar um componente em Visual C# e consumir este mesmo componente em uma aplicação cliente escrita em Visual Basic .NET sem nenhum problema, já que o tipo esperado ou o tipo a ser devolvido são de conhecimento de ambas. Você também ouvirá o termo cross-language integration para essa mesma explicação. Além disso, um outro ponto importante do CTS é que também específica as regras de visibilidade de tipos para acesso aos membros do mesmo. Atualmente temos os seguintes modificadores de acesso disponíveis:

Page 2: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 2

Israel Aece | http://www.projetando.net

2

Modificador VB.NET Modificador C# Descrição Private private Pode ser acessado por outros métodos

somente dentro do mesmo tipo (classe). Protected protected Pode ser acessado por outros métodos

dentro do mesmo tipo e também por tipos derivados.

Friend internal Pode ser chamado de qualquer local desde que seja dentro do mesmo Assembly.

Friend Protected internal protected Pode ser acessado por outros métodos dentro do mesmo tipo e também por tipos derivados dentro do mesmo Assembly.

Public public Pode ser chamado de qualquer local. Para finalizar essa introdução sobre o CTS, há ainda uma das principais e mais importantes regras definidas por ele: todos os tipos devem herdar direta ou indiretamente de um tipo predefinido: System.Object. Este é a raiz de todos os tipos dentro do .NET Framework e, conseqüentemente, será também a raiz para os tipos customizados por você dentro da sua aplicação e, sendo assim, ele fornece um conjunto mínimo de comportamentos:

• Representação em uma string do estado do objeto • Consultar o tipo verdadeiro da instância • Extrair o código hash para a instância • Comparar duas instâncias quanto à igualdade • Realizar uma cópia da instância

CLS – Common Language Specification Graças à um ambiente de execução comum e informações em metadados, a CLR (Common Language Runtime) integra as linguagem e permitem que estas compartilhem os tipos criados em uma linguagem sejam tratado de forma igual na outra. Essa integração é fantástica, mas muitas linguagens são bem diferentes entre si, ou seja, algumas fazem distinção entre maiúsculas e minúsculas, outras não oferecem sobrecargas de operadores, etc.. Se alguém desejar criar um determinado tipo que seja compatível com qualquer outra linguagem .NET, você terá que utilizar somente os recursos que obrigatoriamente estejam também nas outras linguagens. Para isso, a Microsoft criou o Common Language Specification (CLS), que especifica o conjunto mínimo de recursos que devem ser suportados se desejam gerar código para o CLR. Geralmente empresas que estão criando seus compiladores para .NET, devem seguir rigorosamente estas especificações. Para assegurar que o componente que está desenvolvendo seja compatível com qualquer linguagem .NET, você pode incluir um atributo chmado CLSCompliant no arquivo AssemblyInfo que instrui o compilador a se certificar que um tipo público que está sendo

Page 3: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 3

Israel Aece | http://www.projetando.net

3

disponibilizado não contenha nenhuma construção que evite de ser acessado a partir de outra linguagem. O código abaixo exemplifica o uso deste atributo: VB.NET <Assembly:CLSCompliant(True)> C# [Assembly:CLSCompliant(true)] Abaixo, a imagem ilustra o conjunto de tudo que vimos até o momento:

Imagem 1.1 – Uma exibição em forma de conjunto para entermos a relação entre elas.

Tipos fornecidos pelo .NET Framework Dentro do .NET Framework 2.0, várias funcionalidades são encapsuladas dentro de objetos e estes, por sua vez, são instâncias de tipos fornecidos pelo sistema. O .NET Framework já traz internamente vários tipos predefinidos, quais são conhecidos como tipos básicos, ou base system types. Um exemplo típico disso é um controle Button. Internamente ele possui uma porção de membros que expõem ou recebem tipos de dados como strings, inteiros, decimais ou até mesmo outros tipos/objetos. Há alguns tipos de dados que são muito comuns em várias linguagens de programação. Podemos citar vários deles: inteiros, strings, doubles, etc.. São tão comuns que grande parte dos compiladores permitem utilizarmos um código que manipule esses tipos de

Page 4: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 4

Israel Aece | http://www.projetando.net

4

forma simples, um “atalho”. Se esse atalho não existisse, teríamos que fazer algo mais ou menos como: VB.NET Dim codigo As New System.Int32() C# System.Int32 codigo = new System.Int32(); Apesar de funcionar sem nenhum problema, isso não é nada cômodo, justamente pela freqüencia com qual utilizamos esses tipos. Felizmente tanto o Visual Basic .NET quanto o Visual C# fornecem várias keywords que o compilador faz o trabalho de mapear diretamente para o tipo correspondente dentro do .NET Framework Class Library (FCL). Esses tipos de dados que o compilador suporta “diretamente” são chamados de tipos primitivos. Com isso, o código acima ficaria muito mais produtivo se utilizássemos uma sintaxe semelhante a mostrada logo abaixo: VB.NET Dim codigo As Integer C# int codigo = 0; Tipos valor e tipos referência O Common Language Runtime (CLR) suporta duas espécies de tipos: tipos-valor (value types) e tipos-referência (reference types). Os tipos-valor são variáveis que diretamente contém seu próprio valor, sendo assim, cada variável mantém a cópia de seus dados e, conseqüentemente, operações que utilizam essa variável não afetará o seu valor. Tipos-valor são dividos em dois tipos: built-in e user-defined types. O primeiro trata-se de valores que são fornecidos com o próprio .NET Framework, como é o caso de inteiros, floats, etc.. Já o segundo, são tipos que definimos dentro de nossos componentes ou aplicações. Geralmente esses tipos são estruturas de dados ou enumeradores. As estruturas de dados são bem semelhantes a uma classe e podem conter membros para armazenar os dados e também funções para desempenhar algum trabalho. Já os enumeradores são constantes que fixam a escolha de algums dos valores fornecidos por ele, o que impossibilita o consumidor de passar algo diferente do que espera, ou melhor, de passar algum valor que seu método/classe não saiba trabalhar.

Page 5: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 5

Israel Aece | http://www.projetando.net

5

Já os tipos-referência contém ao invés do valor, uma referência para os dados e, sendo assim, é possível termos duas variáveis apontando para o mesmo lugar na memória, ou melhor, para o mesmo objeto. Como os tipos-referência apontam para o mesmo local, uma operação poderá alterar o conteúdo desta variável, já que o mesmo está “compartilhado”. Geralmente todos os objetos dentro do .NET, sejam eles intrínsicos ao Framework ou os objetos customizados, são tipos-referência. Os tipos-referência também são divididos em built-in e user-defined types assim como os tipos-valor. Para exemplificar, built-in type podemos a classe Form (Windows Forms) e a classe Page (Web Forms), entre muitos outros. Para built-in types temos os tipos/objetos que criamos para a aplicação/componente que estamos desenvolvendo. Apesar da imaginação ser o limite, temos alguns exemplos: Cliente, LeitorCNAB, etc.. A hierarquia dos tipos é ilustrada através da imagem 1.2:

Imagem 1.2 – Hierarquia dos tipos do .NET Framework.

Além desta grande diferença entre os dois tipos, ainda há uma separação quando falamos em nível de memória. Cada um dos tipos são armazenados em regiões completamente diferentes. Os tipo-valor são armazenados na Stack e os tipo-referência na Heap. A memória Stack é um bloco de memória alocado para cada programa em runtime. Durante a vida de execução, quando uma função é invocada, todas as variáveis que ela utiliza são colocadas na Stack. Assim que a função é retornada ou o escopo de um bloco é finalizado, o mais breve possível os valores ali colocados serão descartados, liberando assim, a memória que estava sendo ocupada. Como falamos anteriormente, quando uma variável do tipo-valor é passada de uma função, uma cópia do valor é passado e, se esta função alterar este valor, não refletirá no valor original. Como os tipos de dados dentro do .NET Framework são uma estrutura, eles são também armazenados na Stack.

Page 6: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 6

Israel Aece | http://www.projetando.net

6

No caso de tipos-referência, os dados são armazenados na memória Heap, enquanto a referência do mesmo é colocado na mémoria Stack. Isso acontece quando utilizando o operador new (New em VB.NET) no código, que retornará o endereço de memório do objeto. Isso acontece quando precisamos criar efetivamente o objeto para utilizá-lo. Para entender melhor esse processo, vamos analisar o código abaixo: VB.NET Dim cliente1 As Cliente() Dim cliente2 As Cliente = cliente1 C# Cliente cliente1 = new Cliente(); Cliente cliente2 = cliente1; Quando atribuímos o cliente1 ao cliente2 recém criado, você está copiando a referência ao objeto que, por sua vez, aponta para uma determinada seção na memória Heap. No código acima, quando você efetuar uma mudança, seja ela no objeto cliente1 ou cliente2, refletirá no mesmo objeto da memória Heap. Através da imagem 1.2, podemos visualizar as memórias (Stack e Heap) em conjunto quando armazenam tipos-referência:

Imagem 1.3 – Memória Stack e Heap armazenando tipos-referência.

Como já falamos anteriormente, todas os objetos são tipos-referência. Além deles, ainda temos as Interfaces que também operam em modelo tipo-referência. As Interfaces contém métodos e propriedades em seu interior mas não possui nenhuma implementação concreta. Elas devem ser implementadas em classes para implementar a sua funcionalidade. As Interfaces serão discutidas mais detalhadamente ainda neste capítulo. Nota Importante: Todos os tipos em .NET são, em última instância, derivados de System.Object. Como dito anteriormente, todos os tipos do .NET sejam eles intrínsicos ao Framework ou sejam objetos customizados, são tipos-referência. Mas e quanto aos tipos-valor? Não vimos que inteiros, floats são tipo-valor? Sim, mas a questão é que a Microsoft se preocupou com isso e criou uma possibilidade de diferenciar um de outro. Se analisar atentamente a documentação do .NET Framework, verá que a maioria dos tipos-valor, por exemplo integer, decimal, datetime são representados através de

Page 7: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 7

Israel Aece | http://www.projetando.net

7

estruturas de dados: System.Int32, System.Decimal e System.DateTime respectivamente. Toda e qualquer estrutura é derivada de System.ValueType e esta, por sua vez, herda de System.Object. Além da classe System.ValueType fornecer toda a estrutura básica para todos os tipos-valor do .NET ela também é tratada de forma diferente pelo runtime, já que seus tipos derivados devem ser colocados na memória Stack. Estrutura de dados e enumeradores herdam de System.ValueType. Essa distinção entre tipos-valor e tipos-referência deve existir, pois o desempenho de uma aplicação poderia seriamente ser comprometida, pois imagine se a utilização de um simples inteiro, tivéssemos que alocar memória para isso. Os tipos valores são considerados “peso-leve” e, como vimos acima, são alocados na Stack da Thread. As melhorias em termos de performance não param por aí. Esses valores mais “voláteis” não ficam sob inspeção do Garbage Collector e fora do Heap, o que reduz o número de passagens do coletor. Boxing e Unboxing Como vimos na seção anterior, os tipos-valor são muito mais leves que tipos-referência porque não são alocados no Heap e, conseqüentemente, não sofrem coleta de lixo através do Garbage Collector e não são referenciados por ponteiros. Mas imagine uma situação onde você precisa obter uma referência a uma instância do tipo valor. Um caso típico para exemplificar são o uso de coleções. Há dentro do .NET Framework um objeto chamado ArrayList (System.Collections) que, permite-nos criar uma coleção de qualquer tipo. Como ele pode armazenar qualquer tipo, ele aceita nos parâmetros de seus métodos um System.Object (tipos-referência). Como tudo em .NET é, direta ou indiretamente, um tipo de System.Object, posso adicionar nesta coleção qualquer tipo, mesmo sendo um tipo-valor. Mas e se quisermos adicionar um tipo-valor nesta coleção, como por exemplo, uma coleção de inteiros? Para isso, basta simplesmente fazermos: VB.NET Dim colecao As New ArrayList(); colecao.Add(1) colecao.Add(2) C# ArrayList colecao = new ArrayList(); colecao.Add(1); colecao.Add(2); O método Add recebe um tipo-referência (System.Object). Isso quer dizer que o método Add exige uma refência (ponteiro) para um objeto no Heap. Mas no código acima, é

Page 8: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 8

Israel Aece | http://www.projetando.net

8

adicionanado um inteiro e tipo-valor não possui ponteiros para o Heap, já que são armazenados na Stack. Isso só é possível graças ao boxing. Boxing na maioria dos casos ocorre implicitamente. Abaixo é o que acontece nos bastidores quando boxing ocorre:

1. A quantidade de memória é alocada no Heap de acordo com o tamanho do tipo-valor e qualquer overhead a mais, se for o caso.

2. Os campos do tipo-valor são copiados para a Heap que foi previamente alocada. 3. O endereço com a referência é retornado.

Felizmente compiladores das linguagens como Visual Basic .NET e Visual C# produzem automaticamente o código necessário para realizar o boxing. Isso é perfeitamente notável no código acima, pois não precisamos escrever nada mais do que precisaríamos no caso de adicionarmos um tipo-referência. O unboxing é o contrário do boxing, mas considere sendo uma operação mais fácil em relação ao boxing. Unboxing é apenas obter o ponteiro para tipo-valor bruto contido dentro de um objeto e, neste caso, não é necessário copiar nenhum tipo de dado na memória. Obviamente que boxing e unboxing comprometem a performance da aplicação em termos de velocidade como de memória. Felizmente no .NET Framework 2.0, foi introduzido o conceito de Generics Collections que elimina completamente estes problemas. Veremos sobre Generics Collections no Capítulo 2. Examinando tipos especiais Generics Generics é um novo conceito introduzido na versão 2.0 do .NET Framework. Generics permitem termos classes, métodos, propriedades, delegates e Interfaces que trabalhem com um tipo não especificado. Esse tipo “não especificado” quer dizer que estes membros trabalharão com os tipos que você especificar em sua construção. O melhor entendimento deste conceito provavelmente será quando estiver aprendendo sobre Generics Collections, que será abordado no Capítulo 2. Como vimos anteriormente, o ArrayList permite adicionarmos qualquer tipo dentro dele. Mas e se quiséssemos apenas adicionar valores inteiros, ou somente strings? Isso não seria possível pois o método Add aceito um System.Object. Com Generics, é possível criar coleções de um determinado tipo, o que permitirá que o usuário (outro desenvolvedor) somente adicione variáveis do mesmo tipo e, se por acaso ele quiser adicionar algo incompatível, o erro já é detectado em design-time. O .NET Framework 2.0 fornece uma porção de coleções utilizando Generics que veremos mais detalhadamente no Capítulo 2 deste curso. Classes genéricas oferecem várias vantagens, entre elas:

Page 9: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 9

Israel Aece | http://www.projetando.net

9

1. Reusabilidade: Um simples tipo genérico pode ser utilizado em diversos

cenários. Um exemplo é uma classe que fornece um método para somar dois números. Só que estes números podem ser do tipo Integer, Double ou Decimal. Utilizando o conceito de Generics, não precisaríamos de overloads do método Somar(...). Bastaria criar um único método com parâmetros genéricos e a especificação do tipo a ser somado fica a cargo do consumidor.

2. Type-safety: Generics fornecem uma melhor segurança, mais especificamente em coleções. Quando criamos uma coleção genérica e especificamos em seu tipo uma string, somente podemos adicionar strings, ao contrário do ArrayList.

3. Performance: Fornecem uma melhor performance, já que não há mais o boxing e unboxing. Além disso, o número de conversões cai drasticamente, já que tudo passa a trabalhar com um tipo especificado, o que evita transformarmos em outro tipo para termos acesso a suas propriedades, métodos e eventos.

Classes genéricas Uma classe que aceita um tipo em sua declaração pode trabalhar internamente com este tipo. Isso quer dizer que os métodos podem aceitar em seus parâmetros objetos do tipo especificado na criação da classe, retornar esses tipos em propriedades, etc.. Para exemplificarmos, vejamos uma classe que aceita um valor genérico, o que quer dizer, que ela pode trabalhar (fortemente tipada) com qualquer tipo. Através do exemplo abaixo “T” identifica o tipo genérico que, já pode ser acessado internamente pela classe. VB.NET Public Class ClasseGenerica(Of T) Private _valor As T Public Property Valor() As T Get Return Me._valor End Get Set(ByVal value As T) Me._valor = value End Set End Property End Class ‘Utilização: Dim classe1 As New ClasseGenerica(Of String) classe1.Valor = “.NET”

Page 10: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 10

Israel Aece | http://www.projetando.net

10

Dim classe2 As New ClasseGenerica(Of Integer) classe2.Valor = 123 C# public class ClasseGenerica<T> { private T _valor; public T Valor { get { return this._valor; } set { this._valor = value; } } } //Utilização: ClasseGenerica<string> classe1 = new ClasseGenerica<string>(); classe1.Valor = “.NET”; ClasseGenerica<int> classe2 = new ClasseGenerica<int>(); classe2.Valor = 123; O que diferencia uma classe normal de uma classe genérica é o tipo que devemos especificamos durante a criação da mesma. O valor “T” pode ser substituido por qualquer palavra que você achar mais conveniente para a situação e, quando quiser referenciar o tipo genérico em qualquer parte da classe, poderá acessá-lo como um tipo qualquer, como um Integer e felizmente, o Intellisense dá suporte completo a Generics. Digamos que sua classe somente trabalhará com Streams, então poderia definir “T” como “TStream” (se assim desejar): VB.NET Public Class ClasseGenerica(Of TStream) ‘… End Class C# public class ClasseGenerica<TStream> { //… }

Page 11: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 11

Israel Aece | http://www.projetando.net

11

Como podemos notar no exemplo, a classe1 trabalha somente com valores do tipo string. Já a classe2 somente trabalha com valores do tipo inteiro. Mesmo que quiser adicionar um tipo incompatível, o erro já é informado em design-time. Nota: O Visual C# disponibiliza uma keyword bastante útil chamada default. Ela é utilizada em classes genéricas para inicializar um tipo qualquer, pois o Visual C# temos obrigatoriamente que definir um valor default para qualquer tipo, esta vem para suprir esta necessidade. Em casos de tipos-referência, é retornado um valor nulo. Já em casos de valores numéricos, 0 é retornado. Quando o tipo é uma estrutura, é retornado para cada membro desta, 0 ou nulo, dependendo do tipo de cada um. O código abaixo exemplifica o uso desta keyword: C# public class Lista<T> where T : IComparable { public void Add(T item) { T temp = default(T); // .... } } Tipos e Termos Para o entendimento completo de Generics é necessário conhecermos alguns termos que são utilizados durante o uso de Generics. Inicialmente temos dois termos a serem analisados: type parameters e type arguments. O primeiro deles refere-se ao parâmetro que é utilizado na criação do tipo genérico. Já os type arguments referem-se aos tipos que substituem os type parameters. O código abaixo exemplifica essa diferença: VB.NET Public Class ClasseGenerica(Of T) ‘T = type parameter ‘... End Class ‘Utilização Dim c As New ClasseGenerica(Of String) ‘String = type argument C# public class ClasseGenerica<T> //T = type parameter { //... }

Page 12: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 12

Israel Aece | http://www.projetando.net

12

//Utilização ClasseGenerica<string> c = new ClasseGenerica<string>(); //string = type argument Classes genéricas são também chamadas de open types. Levam essa definição porque permitem construirmos uma única classe utilizando diversos tipos. Para ela ser encarada como um open type é necessário que a classe tenha, no mínimo, um tipo a ser especificado. Baseando-se no exemplo acima, a classe ClasseGenerica trata-se de uma open type porque permite, através do type parameter “T”, especificarmos um tipo que a classe irá manipular. As instâncias de classes open types são consideradas constructed types. Além disso, ainda temos mais duas formas de tipos genéricos: open constructed type e close constructed type. A primeira forma, open constructed type, é criado quando, no mínimo, um type argument não é especificado, continuando aberto para uma definição futura. Para transformar essa explicação em código, temos o seguinte exemplo: VB.NET Public Class ClasseGenerica(Of T) ‘... End Class Public Class Derivada(Of T) Inherits ClasseGenerica(Of T) End Class C# public class ClasseGenerica<T> { //... } public class Derivada<T> : ClasseGenerica<T> { //... } Como podemos notar, o tipo ainda continua aberto para que se possa fazer a definição efetiva do tipo mais tarde. Finalmente, temos o close constructed type, que é criado especificando todos os type arguments, não permitindo deixá-lo “aberto” para uma futura definição. O exemplo abaixo ilustra o close constructed type: VB.NET

Page 13: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 13

Israel Aece | http://www.projetando.net

13

Public Class ClasseGenerica(Of K, T) ‘... End Class Public Class Derivada Inherits ClasseGenerica(Of String, Cliente) End Class C# public class ClasseGenerica<K, T> { //... } public class Derivada : ClasseGenerica<string, Cliente> { //... } Mas Generics não param por aqui. Existem muitas outras possibilidades para tornarmos os Generics ainda mais poderosos, como por exemplo critérios (constraints), criação de métodos genéricos, delegates, Interfaces, entre outros, quais veremos a seguir. Métodos genéricos Quando criamos uma classe genérica, informamos o seu tipo logo na construção da mesma. Isso nos habilita a possibilidade de utilizar esse tipo genérico no inteiror da classe em qualquer outro membro. O exemplo abaixo exibe como construir esse metódo: VB.NET Public Class ClasseGenerica(Of T) Public Function RetornaValor(value As T) As T Return value End Function End Class C# public class ClasseGenerica<T> { public T RetornaValor(T value) { return value; } }

Page 14: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 14

Israel Aece | http://www.projetando.net

14

Como pode notar, quando informamos na construção da classe que ela é uma classe genérica, podemos utilizar o tipo “T” em toda a classe. O método RetornaValor recebe esse tipo e o retorna. Quando instanciamos a classe e especificamos o tipo, como por exemplo uma String, todos os membros que utilizam “T” passarão a utilizar String. Felizmente a verificação de tipos é feito em design-time, o que significa que se tentar passar um número inteiro, ele já acusará o erro, sem a necessidade de executar o código para descobrir a possível falha. Através da imagem 1.4 podemos certificar que ao definir o tipo, a classe e o método somente permitirá trabalhar com ele (onde for definido):

Imagem 1.4 – Intellisense dando suporte total a Generics.

Se definirmos o tipo da classe como qualquer outro tipo durante a criação do objeto, onde o “T” estiver especificado será substituido por ele. Só que existem situações onde a classe não é genérica, mas sim somente um método de uma classe qualquer deve suportar parâmetros genéricos. Para isso, há a possibilidade de criarmos métodos genéricos. São basicamente como as classes: definimos o tipo em sua construção e pode-se utilizar tal tipo no interior do respectivo método, seja nos parâmetros ou no retorno do mesmo. Um exemplo da construção de métodos genéricos é exibido abaixo: VB.NET Public Class Classe Public Function RetornaValor(Of T)(ByVal value As T) As T Return value End Function End Class C# public class Classe { public T RetornaValor<T>(T value) { return value; }

Page 15: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 15

Israel Aece | http://www.projetando.net

15

} Para criarmos um método genérico que independe de um tipo especificado na criação da classe, devemos colocar logo após o nome do mesmo o tipo genérico que deverá ser especificado pelo desenvolvedor que o está consumindo. Neste caso, o escopo de utilização de “T” (tipo genérico) passa a ser somente o método. Abaixo é mostrado como utilizá-lo: VB.NET Dim generic As New Classe() Dim id As Integer = generic.RetornaValor(Of Integer)(12) Dim nome As String = generic.RetornaValor(Of String)("José") C# Classe generic = new Classe(); int id = generic.RetornaValor<int>(12); string nome = generic.RetornaValor<string>("José"); Propriedades genéricas As propriedades não tem nada de especial. Elas apenas podem retornar um tipo genérico especificado na criação da classe ou Interface. Um exemplo de sua utilização está no primeiro trecho de código desta seção, que é a propriedade Valor da classe ClasseGenerica. Ela devolve um tipo “T” que é especificado na criação da classe. Interfaces genéricas Assim como as classes, as Interfaces também suportam tipos genéricos. A forma de criação também é idêntica a especificação de tipo genérico da classe e, podemos em seu inteiror, utilizar este tipo. Dentro do .NET Framework 2.0 existem várias Interfaces genéricas e estudaremos algumas delas quando estivermos falando sobre coleções genéricas, no Capítulo 2. Já o exemplo de sua criação e implementação é mostrada através do código abaixo: VB.NET Public Interface ITest(Of T) Property Valor() As T Sub Adicionar(ByVal value As T)

Page 16: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 16

Israel Aece | http://www.projetando.net

16

End Interface Public Class ClasseConcreta Implements ITest(Of String) Public Sub Adicionar(ByVal value As String) _ Implements ITest(Of String).Adicionar End Sub Public Property Valor() As String _ Implements ITest(Of String).Valor Get End Get Set(ByVal value As String) End Set End Property End Class C# public interface ITest<T> { T Valor { get;set; } void Adicionar(T value); } public class ClasseConcreta : ITest<string> { public string Valor { get { } set { } } public void Adicionar(string value) { } } Obviamente que quando implementamos a Interface ITest em uma classe qualquer, obrigatoriamente devemos definir o tipo que a Interface irá trabalhar e, neste caso, o tipo é String. Com isso, todos os tipos genéricos especificados na Interface serão implementados utilizando o tipo String para esta classe.

Page 17: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 17

Israel Aece | http://www.projetando.net

17

Nada impede você de implementar esta mesma Interface em uma outra classe com um tipo diferente, ou ainda, implementar a mesma Interface com tipos diferentes. A única consideração a fazer quando implementamos a mesma Interface em uma classe, pois neste caso, as Intefaces são implementadas explicitamente. Veremos a implementação explícitas de Interfaces ainda neste capítulo. Delegates genéricos Assim como os membros que vimos até o momento, os delegates também permitem serem criados de forma genérica. Basicamente levam também o tipo a ser especificado, o que conseqüentemente, somente permitirá apontar para um membro que obrigatoriamente atende aquele tipo. O exemplo abaixo cria um delegate genérico que aceita um tipo a ser especificado durante a sua criação. Esse delegate poderá ser utilizado para qualquer função que contenha o mesmo número de parâmetros, independentemente do tipo: VB.NET Public Delegate Sub Del(Of T)(ByVal item As T) Sub Main() Dim delInteger As New Del(Of Integer)(AddressOf WriteInteger) Dim delString As New Del(Of String)(AddressOf WriteString) delInteger(123) delString("José") End Sub Public Sub WriteInteger(ByVal i As Integer) Console.WriteLine(i) End Sub Public Sub WriteString(ByVal str As String) Console.WriteLine(str) End Sub C# public delegate void Del<T>(T item); static void Main(string[] args) { Del<int> delInteger = new Del<int>(WriteInteger); Del<string> delString = new Del<string>(WriteString); delInteger(123); delString("José");

Page 18: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 18

Israel Aece | http://www.projetando.net

18

} public static void WriteInteger(int i) { Console.WriteLine(i); } public static void WriteString(string str) { Console.WriteLine(str); } O Visual C# ainda tem alguns atalhos para o código acima. O primeiro deles é o chamado de method group conversion que permite definir o delegate genérico sem a necessidade de instanciá-lo. Sendo assim, o código em C# que faz a definição do método a ser executado quando o delegate for disparado, poderia ser alterado para: C# public delegate void Del<T>(T item); static void Main(string[] args) { Del<int> delInteger = WriteInteger; Del<string> delString = WriteString; delInteger(123); delString("José"); } //Métodos ocultados Para finalizar, o Visual C# ainda tem um forma de tornar mais simples ainda o processo que são os metódos anônimos. Os métodos anônimos são uma novidade do Visual C# 2.0 e permitem que o desenvolvedor defina o código a ser executado pelo delegate diretamente, sem a necessidade de criar um procedimento para isso. Abaixo temos o código em Visual C# modificado que faz o mesmo que vimos anteriormente, mas agora com o uso dos métodos anônimos: C# public delegate void Del<T>(T item); static void Main(string[] args) {

Page 19: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 19

Israel Aece | http://www.projetando.net

19

Del<string> delString = new Del<string>(delegate(string str) { Console.WriteLine(str); }); Del<int> delInteger = new Del<int>(delegate(int i) { Console.WriteLine(i); }); delInteger(123); delString("José"); } Estruturas genéricas Assim como as classes e Interfaces, as estruturas também podem ser genéricas e seguem exatamente as mesmas regras para Generics. Um exemplo típico de estrutura genérica é a Nullable<T>, discutida na próxima seção, ainda neste capítulo. Constraints Vimos até o momento como criar classes, métodos, Interfaces e delegates genéricos. Eles permitem que o consumidor possa definir qualquer tipo. Mas e se quisermos restringir quais tipos podem ser especificados na criação dos Generics? É neste momento que entra em ação as constraints. As constraints são regras que aplicamos aos tipos genéricos para espeficarmos o que o consumidor pode ou não definir como tipo. Atualmente existem seis tipos de constraints: Constraint Descrição where T : struct O tipo especificado deverá ser um value-type, mas precisamente

uma estrutura. Mais uma vez, a estrutura Nullable<T> é um exemplo.

where T : class O tipo especificado deverá ser uma reference-type, como classe, Interface, delegate ou Array.

where T : new() O tipo especificado deverá ter um construtor público sem nenhum parâmetro. Se for utilizado com outras constraints, então esta deverá ser a última.

where T : Base Class O tipo especificado deverá derivar da classe informada. where T : interface O tipo especificado deverá implementar a Interface informada,

podendo inclusive, definir várias constraints desse tipo, o que indica que o tipo deverá obrigatoriamente implementar todas elas.

where T : U Quando há mais de um tipo especificado, podemos definir a constraint onde onde obrigatoriamente deverá herdar do outro.

Page 20: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 20

Israel Aece | http://www.projetando.net

20

As constraints são bastante úteis, pois apesar de garantir a integridade do seu tipo, ou seja, o consumidor não poderá passar um tipo que possa violar a constraint. Além disso, você já é notificado pelo compilador se isso acontecer, que não permite compilar a aplicação com a falha. Para criar as constraints, você utiliza a keyword where (no caso do Visual C#). No Visual Basic .NET, a keyword é As, listando constraint ao lado de constraint para a definição do tipo genérico. Abaixo vamos exemplificar como definir cada uma das constraints disponíveis: where T : struct VB.NET Public Structure Nullable(Of T As Structure) C# public struct Nullable<T> where T : struct where T : class VB.NET Public Class Util(Of T As Class) C# public class Util<T> where T : class where T : new() VB.NET Public Class Util(Of T As New()) C# public class Util<T> where T : new() where T : Base Class VB.NET Public Class Util(Of T As BaseReader) C#

Page 21: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 21

Israel Aece | http://www.projetando.net

21

public class Util<T> where T : BaseReader where T : Interface VB.NET Public Class Util(Of T As IReader) C# public class Util<T> where T : IReader where T : U VB.NET Public Class Util(Of T, U As T) C# public class Util<T, U> where T : U Finalmente, se desejar definir várias constraints em um determinado tipo, basta ir enfileirando uma ao lado da outra, como é mostrado logo abaixo. Somente atente-se para o Visual Basic .NET que, neste caso, exige que você envolva as constraints entre chaves {}: VB.NET Public Class Util(Of T As {IComparable, IReader, New}) C# public class Util<T> where T : IComparable, IReader, new() NullableTypes Qualquer tipo-valor em .NET possui sempre um valor padrão. Isso quer dizer que ele nunca poderá ter um valor nulo e mesmo que tentar, definindo nulo (Nothing em VB.NET e null em C#) para o tipo-valor, o compilador atirará uma exceção. Um exemplo é o DateTime. Ele tem um valor padrão que é 01/01/0001 00:00:00. Apesar de estranha, é uma data válida. Muitas vezes um tipo pode ter uma data não informada, como por exemplo, imagine um objeto Funcionario que tem uma propriedade chamada DataDemissao. Só que este

Page 22: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 22

Israel Aece | http://www.projetando.net

22

funcionario não foi demitido. Então como conseguimos distingüir se essa data é válida ou não? Pois bem, o .NET Framework 2.0 fornece o que chamamos de Nullable Types. System.Nullable<T> é uma estrutura de dados genérica que aceita como tipo qualquer outro tipo, desde que esse tipo seja uma outra estrutura, como por exemplo: Int32, Double, DateTime, etc.. Através deste tipo especial podemos definir valores nulos para ele, como por exemplo: VB.NET Dim dataDemissao As Nullable(Of DateTime) dataDemissao = Nothing C# Nullable<DateTime> dataDemissao = null; A estrutura genérica Nullable<T> fornece também uma propriedade chamada HasValue do tipo booleana que retorna um valor indicando se existe ou não um valor definido. E note que a estrutura Nullable<T> trata-se de uma estrutura genérica, onde o tipo a ser definido obrigatoriamente deve também ser uma estrutura, devido a constraint que obriga isso. Isso pode ser facilmente notado na documentação: VB.NET Public Structure Nullable(Of T As Structure) C# public struct Nullable<T> where T : struct Esses tipos são ideais para utilizá-los junto com registros retornados de uma base de dados qualquer. Apesar de não ter uma grande integração, ajuda imensamente, para conseguirmos mapear as colunas do result-set para as propriedades dos objetos da aplicação que permitem valores nulos. Exceptions Essa é uma das partes mais elegantes do .NET Framework: Exceções e o tratamento delas. Mas afinal, o que é uma exceção: Exceção é uma violação de alguma suposição da interface do seu tipo. Por exemplo, ao projetar um determinado tipo, você imagina as mais diversas situações em que seu tipo será utilizado, difinindo também seus campos, propriedades, métodos e eventos. Como já sabemos, a maneira como você define esses membros, torna-se a interface do seu tipo.

Page 23: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 23

Israel Aece | http://www.projetando.net

23

Assim sendo, dado um método chamado TransferenciaValores, que recebe como parâmetro dois objetos do tipo Conta e um determinado valor (do tipo Double) que deve ser transferido entre elas, precisamos “validá-los” para que a transferência possa ser efetuada com êxito. O desenvolvedor da classe precisará ter conhecimento suficiente para implementar essa tal “validação” e, não esquecer do mais importante: documentar claramente para que os utilizadores deste componente possam implementar o código que fará a chamada ao método da maneira mais eficiente possível, poupando ao máximo que surpresas ocorram em tempo de execução. VB.NET Public Shared Sub TransferenciaValores(de As Conta, para As Conta, valor As Double) ‘... End Sub C# public static void TransferenciaValores(Conta de, Conta para, double valor){ //... } Dentro deste nosso cenário, vamos analisar algumas suposições (“validações”) que devemos fazer para que o método acima possa ser executado da forma esperada:

• Certificar que de e para não são nulos • Certificar que de e para não referenciam a mesma conta • Se o valor for maior que zero • Se o valor é maior que o saldo disponível

Necessitamos agora informar ao chamador que alguma dessas regras foi violada e, como fazemos isso? Atirando uma exceção. Como dissemos logo no começo desta seção, ter uma exceção nem sempre é algo negativo na aplicação, pois o tratamento de exceções permite capturar a exceção, tratá-la e a aplicação continuará correndo normalmente. O tratamento delas dá se de forma estrutura, utilizando o Try/Catch/Finally. Mas nem sempre é necessário ter o overhead desta estrutura, pois há algumas formas de assegurar que o código não dará erros com outros métodos, também fornecidos pelo .NET Framework. Para exemplificar, vamos comparar dois códigos: o primeiro, apesar de resolver o problema, não é a melhor forma; já o segundo pode ser a melhor alternativa para a resolução, ou melhor, para evitar um possível problema: C#

Page 24: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 24

Israel Aece | http://www.projetando.net

24

//Código ruim try { IReader reader = (IReader)e.Data; reader.ExecuteOperation(); } catch { Console.WriteLine(“Tipo incompatível.”); } try { string tempId = “12”; int id = Convert.ToInt32(tempId); this.BindForm(id); } catch { Console.WriteLine(“Id inválido.”); } //Código reformulado IReader reader = (IReader)e.Data as IReader; if(reader != null) { reader.ExecuteOperation(); } else { Console.WriteLine(“Tipo incompatível.”); } string tempId = “12”; int id = 0; if(int.TryParse(tempId, out id)) { this.BindForm(id); } else { Console.WriteLine(“Id inválido.”); } VB.NET

Page 25: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 25

Israel Aece | http://www.projetando.net

25

‘Código ruim Try Dim reader As IReader = CType(e.Data, IReader) reader.ExecuteOperation() Catch Console.WriteLine(“Tipo incompatível.”) End Try Try Dim tempId As String = “12” Dim id As Integer = Convert.ToInt32(tempId) Me.BindForm(id) Catch Console.WriteLine(“Id inválido.”) End Try ‘Código reformulado Dim reader As IReader = TryCast(e.Data, IReader) If Not IsNothing(reader) Then reader.ExecuteOperation() Else Response.Write(“Tipo incompatível.”) End if Dim tempId As String = “12” Dim id As Integer = 0 If Integer.TryParse(tempId, id) Then Me.BindForm(id) Else Console.WriteLine(“Id inválido.”) End if Veremos mais tarde, ainda neste capítulo, sobre os operadores de conversão. Atributos Os atributos são tags declarativas quais podemos decorar nossos membros (assemblies, classes, métodos, propriedades, etc.) de acordo com a necessidade e com a característica do atributo. Esses atributos são armazenados junto aos metadados do elemento e fornecem informações para o runtime que o utilizará para extrair informações em relação ao membro em que é aplicado. Dentro do .NET Framework existem uma porção de atributos, como é o caso do CLSCompliant que vimos no início deste capítulo e também do atributo WebMethod que é utilizado para expor um método via WebService. Há a possibilidade de

Page 26: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 26

Israel Aece | http://www.projetando.net

26

configurarmos inteiramente a segurança de um componente através da forma declarativa, que é justamente utilizando atributos. Para que você possa criar o teu próprio atributo, você obrigatoriamente deve derivar da classe base chamada System.Attribute. Imagine que você tenha uma classe chamada Cliente. Essa classe possui várias propriedades e, entre elas, a propriedade Nome. Você cria uma coleção de clientes e a popula com todos os clientes da sua empresa. Em um segundo momento, você cria um atributo chamado ReportAttribute que pode ser aplicado exclusivamente em propriedades. Esse atributo indica quais propriedades devem fazer parte de um relatório qualquer. Para exemplificar apenas o uso do atributo em conjunto com a propriedade, analise o código a seguir: VB.NET <AttributeUsage(AttributeTargets.Property)> _ Public Class ReportAttribute Inherits Attribute End Class Public Class Cliente Private _nome As String <Report()> _ Public Property Nome() As String Get Return Me._nome End Get Set(ByVal value As String) Me._nome = value End Set End Property ‘ outras propriedades End Class C# [AttributeUsage(AttributeTargets.Property)] public class ReportAttribute : Attribute { } public class Cliente { private string _nome; [Report]

Page 27: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 27

Israel Aece | http://www.projetando.net

27

public string Nome { get { return this._nome; } set { this._nome = value; } } // outras propriedades } Como podemos reparar, na criação da classe ReportAttribute herdamos diretamente da classe Attribute. Denotamos esse atributo customizado com um outro atributo fornecido pelo .NET Framework que é o AttributeUsage que permite especificamos em qual tipo de membro podemos aplicar o atributo. Para o nosso cenário, vamos pode aplicá-lo somente em propriedades. Depois dele pronto, basta simplesmente aplicar o mesmo nas propriedades que qualquer objetos customizado. Claro que precisaríamos ainda de uma função que extrai esses atributos via reflexão (System.Reflection) para assim tomarmos uma decisão baseando-se no atributo. Vale lembrar também que é perfeitamente possível termos propriedades nos atributos customizados para assim passarmos mais informações extras a nível de metadados ou até mesmo informações de regras de negócios. Para finalizar, qualquer membro pode suportar um ou vários atributos. Trabalhando com Interfaces O que são Interfaces? Uma Interface se assemelha a uma classe. Ela pode conter métodos, propriedades e eventos, mas sem qualquer implementação de código, ou seja, somente contém a assinatura dos membros. As Interfaces são implementadas em classes e estas, por sua vez, implementam todo o código para os membros abstratos da Interface, colocando ali o código específico para a classe que a implementa. As Interfaces são úteis para arquiteturas de softwares que exigem um ambiente plug-and-play. Isso quer dizer que uma Interface pode referenciar armazenar uma instância de uma classe concreta desde que esta instância seja um tipo da Interface. Enteda-se por “ser um tipo de” uma classe que implementa a Interface em questão. As Interfaces podem ser implementadas em várias e classes e uma classe pode implementar várias Interfaces. Quando uma classe implementa uma Interface qualquer,

Page 28: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 28

Israel Aece | http://www.projetando.net

28

isso assegurará que o classe fornece exatamente os mesmos membros especificados na Interface implementada de forma pública. A Interface é uma forma de “contrato”, já que deixa explícito exatamente os membros que a classe deverá implementar. O .NET Framework usa Interfaces por toda sua arquitetura e nos permite criar nossas próprias Interfaces. Além disso, o .NET Framework também expõe Interfaces que nos fornecem o “contrato” para implementarmos em nossos objetos para garantir que eles implementem exatamente os membros que são necessários para outros tipos poderem manuseá-lo. Entre as inúmeros Interfaces fornecidas, vamos analisar algumas delas, as mais úteis ao nosso dia-à-dia e entender como e quando implementá-las. Quando falamos de implementação de Interfaces, há dois tipos:

1. Normal: Esta opção é a mais tradicional, pois implementamos a Interface em uma classe qualquer e os métodos, propriedades e eventos definidos pela Interface são publicamente disponibilizados também pela classe.

2. Explícita: A opção implícita permite-nos implementar várias Interfaces com membros com o mesmo nome em um determinado objeto. Isso é um caso comum, pois há situações onde a Interface tem um método comum com outra Interface mas ambas precisam ser implementadas.

A implementação explícita são suportadas tanto no Visual Basic .NET quanto no Visual C#, mas há uma pequena diferença: o Visual C# prefixa o nome do método com o nome da Interface correspondente; já o Visual Basic .NET cria nomes diferentes para o método, mas permite invocá-lo através da Interface. Além disso, o Visual Basic .NET ainda permite que se invoque os métodos através da instância da classe o que não é permitido no Visual C#. Abaixo é exibido um exemplo deste cenário: VB.NET Public Interface IReader Sub Start() End Interface Public Interface IWriter Sub Start() End Interface Public Class Process Implements IReader Implements IWriter Public Sub Start() Implements IReader.Start Console.WriteLine("IReader.Start") End Sub Public Sub Start1() Implements IWriter.Start

Page 29: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 29

Israel Aece | http://www.projetando.net

29

Console.WriteLine("IWriter.Start") End Sub End Class C# public interface IReader { void Start(); } public interface IWriter { void Start(); } public class Process : IReader, IWriter { void IReader.Start() { Console.WriteLine("IReader.Start"); } void IWriter.Start() { Console.WriteLine("IWriter.Start"); } } Como podemos ver, as Interfaces IReader e IWriter possui um mesmo método chamado Start. A única observação é que no caso do Visual Basic .NET ele necessariamente precisa ter um nome diferente na classe concreta Process. No caso do Visual C#, o prefixo do método, que é o nome da Interface, garante a distinção entre elas. As diferenças não param aí. No caso do Visual C# esses métodos somente são acessíveis se forem invocados através da própria Interface. Isso quer dizer que, através da instância da classe Process não está disponível e, se reparar, o Intellisense também não suporta. Isso já é possível no Visual Basic .NET. O código abaixo mostra a utilização da classe Process: VB.NET Dim p As New Process Dim reader As IReader = p Dim writer As IWriter = p reader.Start() writer.Start() ‘Mas também é possível fazer: p.Start() p.Start1()

Page 30: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 30

Israel Aece | http://www.projetando.net

30

C# Process p = new Process(); IReader reader = (IReader)p; IWriter writer = (IWriter)p; reader.Start(); writer.Start(); Esse tipo de implementação somente se faz necessária quando há necessidade de implementar duas Interfaces em um objeto que contém exatamente um mesmo membro. Um exemplo disso dentro do .NET Framework são as estruturas de dados, como por exemplo: Int32, Double, etc., elas implementam explicitamente a Interface IConvertible. Conversão de tipos utilizando IConvertible Imagine que esteja desenvolvendo uma aplicação em que contém uma classe chamada CalculoDeMedicoesPredio e dentro dela toda a lógica para calcular as medições de uma construção qualquer. Agora, você precisa deste valor para gerar outras informações para o usuário, como por exemplo relatórios ou até mesmo gráficos. Geralmente, os módulos que esperam esses valores (que talvez não seja nem a sua aplicação, pode ser um componente de terceiros) exigem que os valores estejam em um determinado tipo. Sendo assim, como eu faço para converter meu tipo complexo (CalculoDeMedicoesPredio) em um tipo esperado pelo módulo para geração de relatórios e gráficos? O .NET Framework fornece uma Interface chamada IConvertible que permite-nos implementá-la em nossos objetos. Essa Interface possuiu uma série de métodos que permitem converter um objeto em um tipo fornecedido pelo .NET Framework, a saber: Boolean, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, Decimal, DateTime, Char, e String. Os nomes dos métodos da Interface em questão são basicamente os mesmos, apenas prefixados com To. Exemplo: ToBoolean, ToDateTime, ToString, etc.. Como já devem desconficar, implementando a Interface IConvertible em seu tipo, poderá utilizá-lo passando para o método correspondente a conversão que quer fazer para a classe Convert. Um ponto importante aqui é que você precisa antentar-se é para quais tipos o seu objeto pode ser convertido. No caso acima, não faz sentindo convertermos a classe CalculoDeMedicoesPredio para Boolean. Nos tipos “incompatíveis” o ideal é sempre você atirar uma exceção do tipo InvalidCastException para não permitem conversões não suportadas. Através do código abaixo é possível analisar como implementar a Interace IConvertible:

Page 31: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 31

Israel Aece | http://www.projetando.net

31

VB.NET Public Class CalculoDeMedicoesPredio Implements IConvertible Private _altura As Double Private _largura As Double Public Sub New(ByVal largura As Double, _ ByVal altura As Double) Me._altura = altura Me._largura = largura End Sub Public Function ToBoolean(ByVal provider As IFormatProvider) As Boolean Implements IConvertible.ToBoolean Throw New InvalidCastException End Function Public Function ToDouble(ByVal provider As IFormatProvider) As Double Implements IConvertible.ToDouble Return Me._altura * Me._largura End Function 'demais métodos End Class ‘Utilização: Dim calculo As New CalculoDeMedicoesPredio(10, 30) Dim area As Double = Convert.ToDouble(calculo) Console.WriteLine(area) C# public class CalculoDeMedicoesPredio : IConvertible { private double _largura; private double _altura; public CalculoDeMedicoesPredio(double largura, double altura) { this._altura = altura; this._largura = largura; } public bool ToBoolean(IFormatProvider provider) { throw new InvalidCastException;

Page 32: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 32

Israel Aece | http://www.projetando.net

32

} public double ToDouble(IFormatProvider provider) { return this._altura * this._largura; } // demais métodos } //Utilização: CalculoDeMedicoesPredio calculo = new CalculoDeMedicoesPredio(10, 30); double area = Convert.ToDouble(calculo); Console.WriteLine(area); IComparer, IComparable e IEquatable Geralmente quando estamos trabalhando com objetos customizados dentro da aplicação, é muito normal querermos exibir tais objetos para o usuário. Para exemplificar, teremos um objeto do tipo Usuario que possui uma propriedade do tipo Nome. Imagine que você tenha uma lista de usuários com nomes aleatórios, ou seja, sem uma ordenação específica. Essa lista conterá objetos do tipo Usuario com o seu respectivo nome. A aplicação se encarrega de carregar essa lista e chega o nomento em que você precisa exibí-la em um controle qualquer e, é necessário que essa lista seja exibida aplicando uma ordenação alfabética na mesma. Como proceder para customizar a ordenação? Pois bem, várias coleções no .NET Framework, como por exemplo List<T>, ArrayList, etc., fornecem um método chamado Sort. Só que apenas invocar este método para ordenar a lista não é o suficiente. É necessário que você implemente as Interfaces IComparer<T> (IComparer) e IComparable<T> (IComparable) para isso. A primeira delas, IComparer<T>, customiza a forma de como ordenar a coleção; já a segunda, IComparable<T>, é utilizada como definir como a ordenação é realizada em uma classe específica. Inicialmente iremos analisar a Interface IComparable<T>. Ela fornece um método chamado CompareTo, que permite comparar a instância da uma classe onde esta Interface está sendo implementando com um objeto (do mesmo tipo) que é passado para o método. Com a versão 2.0 do .NET Framework foi criada uma versão nova desta mesma Interface IComparable, que é a IComparable<T>. A diferença em relação a IComparable é que permite trabalharmos com Generics, o que melhora muito a performance, já que não é necessário efetuarmos a conversão de System.Object para a classe que estamos utilizando e, somente depois disso, compararmos.

Page 33: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 33

Israel Aece | http://www.projetando.net

33

Com todos os conceitos definidos, resta-nos partir para a implementação concreta para analisarmos: VB.NET Public Class Usuario Implements IComparable(Of Usuario) Private _nome As String Public Sub New(ByVal nome As String) Me._nome = nome End Sub Public Property Nome() As String Get Return Me._nome End Get Set(ByVal value As String) Me._nome = value End Set End Property Public Function CompareTo(ByVal other As Usuario) As _ Integer Implements IComparable(Of Usuario).CompareTo Return Me.Nome.CompareTo(other.Nome) End Function End Class C# public class Usuario : IComparable<Usuario> { private string _nome; public Usuario(string nome) { this._nome = nome; } public string Nome { get { return _nome; } set { _nome = value; } } public int CompareTo(Usuario other) { return this.Nome.CompareTo(other.Nome);

Page 34: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 34

Israel Aece | http://www.projetando.net

34

} } Ao especificar a implementação da Interface IComparable<T>, já definimos que esta Interface deverá trabalhar com o tipo Usuario, que é justamente a classe onde estamos implementando-a. Dentro do método CompareTo fornecido por ela comparamos a propriedade Nome da instância corrente com a propriedade Nome da instância que vem como parâmetro para método. Reparem que utilizando a Interface genérica não é necessário efetuar a conversão dentro do método para, em seguinda, efetuar a comparação, pois o objeto já está com o tipo especificado na implementação da Interface. Ao exibir a coleção depois de invocar o método Sort, o resultado será o seguinte: VB.NET Dim list As New List(Of Usuario) list.Add(New Usuario("Virginia")) list.Add(New Usuario("Zuleika")) list.Add(New Usuario("Ana")) list.Add(New Usuario("Elisabeth")) list.Add(New Usuario("Tiago")) list.Add(New Usuario("Humberto")) list.Sort() ‘Output Ana Elisabeth Humberto Tiago Virginia Zuleika C# List<Usuario> list = new List<Usuario>(); list.Add(new Usuario("Virginia")); list.Add(new Usuario("Zuleika")); list.Add(new Usuario("Ana")); list.Add(new Usuario("Elisabeth")); list.Add(new Usuario("Tiago")); list.Add(new Usuario("Humberto")); list.Sort(); //Output Ana Elisabeth Humberto Tiago

Page 35: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 35

Israel Aece | http://www.projetando.net

35

Virginia Zuleika Um ponto bastante importante que deve ser comentado com relação ao código acima é que, ao invocar o método Sort e o objeto especificado não implementar a Interface IComparable, uma exceção do tipo InvalidOperationException será lançada. Como o exemplo é um tanto quanto simples, pois tem apenas uma propriedade e, conseqüentemente, um único campo de ordenação, como ficaria se quiséssemos disponibilizar várias formas para ordenação? Exemplo: inicialmente a listagem é exibida com os nomes em ordem alfabética. Em seguida, gostaria de exibir a mesma listagem, mas agora ordenando por grupo. Para isso, devemos utilizar a Interface IComparer ou IComparer<T>. Essa Interface fornece um método denominado Compare, que compara dois objetos retornando um número inteiro indicando se um objeto é menor, igual ou maior que o outro objeto. Esta Interface deverá ser implementada em uma classe diferente da atual (Usuario), uma espécie de classe de “utilidades” para executar o seu trabalho. O primeiro passo será a criação da classe que implementará a Interface IComparer<T> e, sem seguida, codificar o método Compare. Devemos ainda criar um enumerador para que o consumidor da classe possa escolher por qual campo ele deseja ordenar a listagem. A implementação é exibida abaixo: VB.NET Public Class UsuarioSorting Implements IComparer(Of Usuario) Public Enum SortType Nome Grupo End Enum Private _sortType As SortType Public Sub New(ByVal sortType As SortType) Me._sortType = sortType End Sub Public Function Compare(ByVal x As Usuario, ByVal y As Usuario) _ As Integer Implements IComparer(Of Usuario).Compare Select Case Me._sortType Case SortType.Grupo Return x.Grupo.CompareTo(y.Grupo) Case SortType.Nome

Page 36: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 36

Israel Aece | http://www.projetando.net

36

Return x.Nome.CompareTo(y.Nome) End Select Return 0 End Function End Class C# public class UsuarioSorting : IComparer<Usuario> { public enum SortType { Nome, Grupo } private SortType _sortType; public UsuarioSorting(SortType sortType) { this._sortType = sortType; } public int Compare(Usuario x, Usuario y) { switch (this._sortType) { case SortType.Grupo: return x.Grupo.CompareTo(y.Grupo); case SortType.Nome: return x.Nome.CompareTo(y.Nome); } return 0; } } A classe UsuarioSorting é responsável por receber qual o tipo de ordenação e, baseado nele irá determinar qual das propriedades devem ser comparadas para montar a ordenação. Finalmente, a utilizadação desta classe muda ligeiramente ao que vimos anteriormente durante o exemplo da Interface IComparable<T>. A diferença resume a informar para um dos overloads do método Sort da coleção um objeto que customiza o ordenação (que obrigatoriamente deve implementar a Interface IComparer<T>): VB.NET ‘ordenar por nome usuarios.Sort(New UsuarioSorting(UsuarioSorting.SortType.Nome))

Page 37: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 37

Israel Aece | http://www.projetando.net

37

‘ordenar por grupo usuarios.Sort(New UsuarioSorting(UsuarioSorting.SortType.Grupo)) C# //ordenar por nome usuarios.Sort(new UsuarioSorting(UsuarioSorting.SortType.Nome)); //ordenar por grupo usuarios.Sort(new UsuarioSorting(UsuarioSorting.SortType.Grupo)); Nota: Quando ambas Interfaces (IComparable<T> e IComparer<T> são implementadas, somente a Interface IComparer<T> é utilizada. Ainda temos a Interface IEquatable<T> que compara dois objetos quanto à igualdade, fazendo basicamente o que já faz o método Equals de System.Object, mas assim como o IComparable<T>, com uma vantagem: trabalha com o tipo específico e não System.Object, o que evita o boxing/unboxing. Essa Interface tem um método chamado Equals que recebe como parâmetro um objeto do mesmo tipo especificado na declaração da Interface. A implementação da Interface é exibida abaixo: VB.NET Public Class Usuario Implements IEquatable(Of Usuario) Private _nome As String Public Property Nome() As String Get Return Me._nome End Get Set(ByVal value As String) Me._nome = value End Set End Property Public Function Equals1(ByVal other As Usuario) As _ Boolean Implements IEquatable(Of Usuario).Equals If IsNothing(other) Then Return False Return Me.Nome = other.Nome End Function End Class C#

Page 38: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 38

Israel Aece | http://www.projetando.net

38

public class Usuario : IEquatable<Usuario> { private string _nome; public Usuario(string nome) { this._nome = nome; } public string Nome { get { return _nome; } set { _nome = value; } } public bool Equals(Usuario other) { if(other == null) return false; return other.Nome == this.Nome; } } É natural que ao utilizar a classe e chamar o método Equals, verá que na lista de overloads aparecerá duas versões do mesmo método. Uma delas espera exatamente o tipo informado na Interface IEquatable<T>, enquanto a outra espera um System.Object, que obviamente herda do também de System.Object. Copiando a referência de um objeto utilizando ICloneable Quando falamos a respeito de tipos-referência citamos que quando atribuímos uma instância de um objeto qualquer a um objeto do mesmo tipo recém criado, ambas passam a apontar (ter a mesma referência) ao mesmo objeto na memória Heap. Sendo assim, qualquer mudança efetuada em algum dos objetos, será imediatamente refletida no outro. Mas pode existir situações onde será necessário criarmos uma cópia, ou melhor, um clone de objeto para que essa operação não interfira em seu original. Para assegurar isso, o Microsoft disponibilizou uma Interface chamada ICloneable que cria uma nova instância do objeto em questão com os mesmo valores da instância corrente. Esse processo é chamado de “clonar” o objeto e, como já era de se esperar, a Interface fornece um método chamado Clone para efetuarmos essa operação. Mas, quando falamos em clone, existem dois tipos:

1. Shallow-cloning: Este tipo de clone se resume a copiar apenas os valores, sem copiar qualquer referência a outros objetos que existam internamente ao objeto.Com isso, tipos-referência continuaram apontando para o mesmo local, mesmo no clone.

Page 39: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 39

Israel Aece | http://www.projetando.net

39

2. Deep-cloning: Este tipo de clone, além de copiar os valores, também copia os membros que são tipo-referências, criando assim um novo objeto completamente independente.

Através das imagens abaixo conseguimos comparar as diferências entre os tipos de clones suportados:

Imagem 1.5 – Shallow-Clone – Os tipos-referência ainda continuam apontando para o

mesmo local.

Imagem 1.6 – Deep-Clone – Uma cópia completa do objeto.

A classe System.Object fornece um método protegido (protected) chamado MemberwiseClone. Este método retorna um clone no estilo Shallow, ou seja, copiará os valores e apenas as referência para os membros tipo-referência, se existirem. Dependendo do cenário isso não é muito interessante. Se desejar mesmo fazer um clone do tipo Deep-Clonning, será necessário implementar a Interface ICloneable e customizar o método Clone. A implementação não é complexa e podemos comprovar através do código abaixo:

Page 40: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 40

Israel Aece | http://www.projetando.net

40

VB.NET Public Class Cliente Private _nome As String Public Sub New(ByVal nome As String) Me._nome = nome End Sub Public Property Nome() As String Get Return Me._nome End Get Set(ByVal value As String) Me._nome = value End Set End Property End Class Public Class Pedido Implements ICloneable Private _id As Integer Private _data As DateTime Private _cliente As Cliente Public Sub New(ByVal id As Integer, ByVal data As DateTime) Me._data = data Me._id = id End Sub Public Property Cliente() As Cliente Get Return Me._cliente End Get Set(ByVal value As Cliente) Me._cliente = value End Set End Property Public Function Clone() As Object _ Implements System.ICloneable.Clone Dim pedidoClone As New Pedido(Me._id, Me._data) pedidoClone.Cliente = New Cliente(Me.Cliente.Nome) Return pedidoClone End Function

Page 41: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 41

Israel Aece | http://www.projetando.net

41

End Class ‘Utilização: Dim pedido1 As New Pedido(1, DateTime.Now) pedido1.Cliente = New Cliente("José Torres") Dim pedido2 As Pedido = DirectCast(pedido1.Clone(), Pedido) pedido2.Cliente.Nome = "Maria Torres" Console.WriteLine(pedido1.Cliente.Nome) Console.WriteLine(pedido2.Cliente.Nome) ‘Output: ‘José Torres ‘Maria Torres C# public class Cliente { private string _nome; public Cliente(string nome) { this._nome = nome; } public string Nome { get { return this._nome; } set { this._nome = value; } } } public class Pedido : ICloneable { private int _id; private DateTime _data; private Cliente _cliente; public Pedido(int id, DateTime data) { this._data = data; this._id = id;

Page 42: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 42

Israel Aece | http://www.projetando.net

42

} public Cliente Cliente { get { return this._cliente; } set { this._cliente = value; } } public object Clone() { Pedido clone = new Pedido(this._id, this._data); clone.Cliente = new Cliente(this.Cliente.Nome); return clone; } } //Utilização: Pedido pedido1 = new Pedido(1, DateTime.Now); pedido1.Cliente = new Cliente("José Torres"); Pedido pedido2 = (Pedido)pedido1.Clone(); pedido2.Cliente.Nome = "Maria Torres"; Console.WriteLine(pedido1.Cliente.Nome); Console.WriteLine(pedido2.Cliente.Nome); //Output: //José Torres //Maria Torres Como podemos notar, o clone copiou na íntegra todos os valores do pedido1, mesmo os valores que são tipos-referência. Agora, se apenas modificarmos a implementação do método Clone retornando a chamada para o método MemberwiseClone de System.Object, veremos que apenas a referência para o objeto Cliente é copiado e, sendo assim, ao alterar a propriedade Nome do pedido2 refletirá também no pedido1. Formatando um tipo em uma string com IFormattable Já vimos e utilizamos várias vezes padrões de formatação que são fornecidos intrinsicamente pela infraestrutura do .NET Framework desde a sua versão 1.x. Esses padrões são comuns quando necessitamos formatar datas ou números, para exibirmos ao

Page 43: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 43

Israel Aece | http://www.projetando.net

43

usuário um valor mais amigável e, alguns exemplos típicos estão neste artigo. Mas há situações em que precisamos formatar o nosso objeto customizado e, felizmente, o .NET Framework fornece uma interface chamada IFormattable qual possui um único método denominado, ToString qual é invocado automaticamente pelo runtime quando especificamos uma formatação. Para o nosso cenário de exemplo, teremos dois objetos: Cliente e Cnpj. Cada cliente obrigatoriamente terá uma propriedade do tipo Cnpj. Este tipo por sua vez, implementará a interface IFormattable e deverá fornecer dois tipos de formatação: DF e PR. O primeiro significa “Documento Formatado” e retornará o valor do CNPJ formatado; já a segunda opção significa “Prefixo” e, se o usuário optar por este tipo de formatação, será retornado apenas o prefixo do CNPJ que, para quem não sabe, trata-se dos 9 primeiros dígitos. A arquitetura de classes que servirá como exemplo: VB.NET Public Class Cliente Private _nome As String Private _cnpj As Cnpj Public Sub New(nome As String, cnpj As String) Me._nome = nome Me._cnpj = New Cnpj(cnpj) End Sub Public Property Nome() As String Get Return Me._nome End Get Set(Value As String) Me._nome = value End Set End Property Public Property Cnpj() As Cnpj Get Return Me._cnpj End Get Set(Value As Cnpj) Me._cnpj = value End Set End Property End Class Public Class Cnpj Implements IFormattable Private _numero As String

Page 44: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 44

Israel Aece | http://www.projetando.net

44

Public Sub New(numero As String) Me._numero = numero End Sub Public Overloads Function ToString(format As String, _ formatProvider As IFormatProvider) As String _ Implements IFormattable.ToString If format = "DF" Then Return Convert.ToDouble(Me._numero).ToString("000\.000\.000\/0000\-00") Else If format = "PR" Then Return Convert.ToDouble(Me._numero.Substring(0, 9)).ToString("000\.000\.000") End If Return Me._numero End Function Public Overrides Function ToString() As String Return Me.ToString(Nothing, Nothing) End Function End Class ‘Utilização: Dim c As New Cnpj("999999999999999") Response.Write(string.Format("O documento formatado é {0:DF}.", c)) Response.Write(string.Format("O prefixo é {0:PR}.", c)) Response.Write(string.Format("O documento é {0}.", c)) ' Output: ' O documento formatado é 999.999.999/9999-99. ' O prefixo é 999.999.999. ' O documento é 999999999999999. C# public class Cliente { private string _nome; private Cnpj _cnpj; public Cliente(string nome, string cnpj) { this._nome = nome; this._cnpj = new Cnpj(cnpj); }

Page 45: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 45

Israel Aece | http://www.projetando.net

45

public string Nome { get { return this._nome; } set { this._nome = value; } } public Cnpj Cnpj { get { return this._cnpj; } set { this._cnpj = value; } } } public class Cnpj : IFormattable { private string _numero; public Cnpj(string numero) { this._numero = numero; } public string ToString(string format, IFormatProvider formatProvider) { if (format == "DF") return Convert.ToDouble(this._numero).ToString(@"000\.000\.000\/0000\-00"); else if (format == "PR") return Convert.ToDouble(this._numero.Substring(0, 9)).ToString(@"000\.000\.000"); return this._numero; }

Page 46: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 46

Israel Aece | http://www.projetando.net

46

public override string ToString() { return this.ToString(null, null); } } //Utilização: Cnpj c = new Cnpj("999999999999999"); Response.Write(string.Format("O documento formatado é {0:DF}.", c)); Response.Write(string.Format("O prefixo é {0:PR}.", c)); Response.Write(string.Format("O documento é {0}.", c)); // Output: // O documento formatado é 999.999.999/9999-99. // O prefixo é 999.999.999. // O documento é 999999999999999. Como podemos reparar, a classe Cliente tem uma propriedade chamada Cnpj do tipo Cnpj. O objeto Cnpj implementa a Interface IFormattable e, dentro do método ToString, verifica qual o formato está sendo passado para ele. Baseando-se neste formato é que uma determinada formatação é aplicada ao número do CNPJ e, caso nenhuma formatação é especificada, somente o número é retornado, sem nenhuma espécie de formatação. Esse tipo de formatação torna tudo muito flexível e, podemos ainda especificar o tipo de formatação em controles DataBound, como por exemplo o GridView do ASP.NET, da mesma forma que fazemos para datas e números. As imagens abaixo ilustram isso:

Imagem 1.7 – Aplicando formatação em controles.

Liberando referências a objetos não gerenciados através da interface IDisposable

Page 47: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 47

Israel Aece | http://www.projetando.net

47

Utilizar o método Finalize disponibilizado pela classe System.Object é muito útil, pois assegura que recursos não gerenciados não fiquem “perdidos” quando objetos gerenciados é descartado. Só que há um problema neste cenário: não é possível saber quando o método Finalize é chamado e o fato deste método não ser público, não há a possibilidade de invocá-lo explicitamente. Imagine o seguinte cenário: a conexão com um banco de dados qualquer é extremamente custosa e deixar ela aberta até que o runtime determine que ela seja descartada, podemos ter vários danos, pois isso pode acontecer até alguns dias depois. Para termos a possibilidade de finalizar o objeto deterministicamente e, ali fecharmos conexões com bancos de dados e arquivos a Microsoft implementou um padrão chamado Dispose. Através de uma Interface chamada IDisposable, que contém um único método chamado Dispose, você pode implementar na sua classe de acesso a dados, arquivos, MessageQueue, entre outros recursos para que o recurso seja explicitamente fechado quando o processo finalizar, sem a necessidade de aguardar que o Garbage Collector decida fazer isso. Na maioria dos cenário onde se utilizar o padrão Dispose, dentro da implementação do método invoca-se um outro método chamado SuppressFinalize da classe GC (Garbage Collector). Teoricamente como todos os recursos são finalizados dentro do método Dispose, não há necessidade do runtime invocar o método Finalize, justamente porque não tem mais nada a fazer, pois o objeto já foi descartado. Abaixo podemos visualizar uma classe customizada que implementa o padrão Dispose: VB.NET Public Class Reader Implements IDisposable Private _reader As StreamReader Private disposed As Boolean Public Sub New(ByVal filename As String) Me._reader = New StreamReader(filename) End Sub Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not Me.disposed Then If disposing Then If Not IsNothing(Me._reader) Then Me._reader.Dispose() End If End If Me.disposed = True End If

Page 48: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 48

Israel Aece | http://www.projetando.net

48

End Sub Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub End Class C# public class Reader : IDisposable { private StreamReader _reader; private bool disposed = false; public Reader(string filename) { this._reader = new StreamReader(filename); } protected virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { if (this._reader != null) { this._reader.Dispose(); } } disposed = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } Analisando o código acima, nota-se que existem dois métodos Dispose. O método que não tem nenhum parâmetro é o método fornecido pela Interface IDisposable. Além dele, criamos um método protegido (protected) e que pode ser sobrescrito nas classes derivadas que faz a análise dos recursos que devem ser descartados quando o método público Dispose é chamado.

Page 49: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 49

Israel Aece | http://www.projetando.net

49

Além disso, termos uma classe que implementa IDisposable é interessante porque podemos ainda utilizar em conjunto com o bloco using. Isso quer dizer que, ao utilizar o objeto dentro de um bloco using, o runtime se encarrega de invocar o método Dispose mesmo que alguma exceção ocorra até o término da operação e sem termos o código envolvido em blocos Try/Catch/Finally. Isso se dá porque o compilar de encarrega de transformar o bloco using em um bloco envolvido por Try/Finally. Para finalizar, o código abaixo exibe o consumo da classe Reader que foi construída acima com o padrão IDisposable. A classe será utilizada em conjunto com o bloco using. Isso fará com que, ao chegar no término do bloco, o método Dispose é chamado automaticamente pelo runtime: VB.NET Using reader As New Reader("C:\Temp.txt") 'executa outras operações End Using C# using (Reader reader = new Reader("C:\\Temp.txt")) { //executa outras operações } Casting Efetuando conversões em tipos-referência Uma das partes mais importantes do Common Language Runtime (CLR) é a segurança de tipos. Em runtime, o CRL sempre sabe qual tipo o objeto realmente é e, se em algum momento você quiser saber, basta chamar o método GetType. Cast (ou conversão) é algo que os desenvolvedores utilizam imensamente na aplicação, convertendo um objeto em vários outros tipos diferentes (desde que suportados). Geralmente, o cast para o tipo base não exige nenhum tipo especial. Para citar um exemplo, você poderia fazer atribuir a uma variável do tipo System.Object a instância de uma classe Cliente, podendo utilizar o seguinte código: VB.NET Dim cliente As New Cliente() cliente.Nome = “José Torres” Dim obj As Object = cliente

Page 50: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 50

Israel Aece | http://www.projetando.net

50

C# Cliente cliente = new Cliente(); cliente.Nome = “José Torres”; object obj = cliente; Para fazer o inverso, ou seja, extrair da variável obj o Pedido que está dentro dela, é necessário efetuar o cast se estiver utilizando C#. O Visual Basic .NET não exige que você faça isso explicítamente, pois ele gera o código necessário para tal operação. No entanto é sempre menos performático fazer dessa forma e, além disso, dificulta encontrarmos possíveis problemas ainda em compile-time, ou seja, se o tipo não for compatível, somente descobriremos isso quando a aplicação for executada e uma exceção for atirada. Para melhorar isso no Visual Basic .NET, vá até as propriedades do projeto no Solution Explorer, em seguida na aba Compile e defina a opção Option Strict como On. Isso obrigará o desenvolvedor a fazer as conversões explícitas no código. Para extrair o pedido da variável obj, o código é o seguinte: VB.NET Dim pedidoNovo As Pedido = DirectCast(obj, Pedido) C# Pedido pedidoNovo = (Pedido)obj; O código acima somente resultará com sucesso se o valor contido dentro da variável obj for realmente um Pedido. Do contrário, a aplicação retornará um exceção do tipo InvalidCastException, informando que não é possível converter o que está em obj para Pedido. Para evitar que problemas como este ocorra em tempo de execução, tanto o Visual Basic .NET quando o Visual C# fornecem alguns operadores que permitem que a conversão seja feita de forma “mais segura”, evitando assim, o problema que identificamos acima. Felizmente para ambas as linguagens temos um operador que, se a conversão não for satisfeita, ao invés de atirar uma exceção, retornar uma valor nulo e, conseqüentemente, você deve tratar isso para dar continuidade na execução do código. Estamos falando do operador as para o Visual C# e o operador TryCast para o Visual Basic .NET. Para exemplificar o uso destes operadores, vamos transformar o exemplo que fizemos logo acima para utilizar os operadores em questão: VB.NET Dim pedidoNovo As Pedido = TryCast(obj, Pedido) If Not IsNothing(pedidoNovo) Then Console.WriteLine(pedidoNovo.Data)

Page 51: Livro de .NET - Israel Aece

Capítulo 1 – Tipos de dados e Interfaces 51

Israel Aece | http://www.projetando.net

51

End If C# Pedido pedidoNovo = obj as Pedido; if(pedidoNovo != null) { Console.WriteLine(pedidoNovo.Data); } É importante dizer que ao utilizar esses operadores, de nada vai adiantar se não verificar se está ou não nulo, pois se por algum motivo retornar nulo e você invocar algum membro deste objeto, o runtime atirará uma exceção do tipo NullReferenceException.

Page 52: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 1

Israel Aece | http://www.projetando.net

1

Capítulo 2 Trabalhando com Coleções Introdução Desde a primeira versão do .NET Framework existem classes que possibilitam armazenarmos outros objetos e fornecem toda a funcionalidade para interagir com estes objetos. Essas coleções obviamente foram mantidas, mas a Microsoft criou um novo subset de coleções chamado Generics que possibilita ao desenvolvedor escrever um código muito mais limpo e robusto. No decorrer deste artigo veremos as coleções disponíveis dentro do .NET Framework 2.0. O capítulo começa mostrando as coleções existentes desde a versão 1.0 do mesmo e, avançando um pouco mais, analisaremos as coleções genéricas disponíveis na versão 2.0 que é uma funcionalidade que trouxe uma grande flexibilidade e uma melhoria notável em termos de performance e produtividade na escrita do código. Não veremos apenas as coleções concretas, mas também vamos entender qual a arquitetura disponível e as Interfaces necessárias e disponíveis para a criação de coleções customizadas. Ambas seções onde são abordados cada tipo de coleção, será abordado as Interfaces disponíveis para efetuarmos a comparação entre objetos. Depois de analisar as coleção primárias e as coleções genéricas, vamos abordar as coleções especializadas, que são criadas para uma finalidade muito específica e também classes que fornecem as funcionalidades básicas para a criação de coleções customizadas para uma determinado cenário. O que são Coleções? Coleções são classes que armazenam em seu interior os mais diversos tipos de objetos. Elas são muito mais fácil de manipular em relação a Arrays, justamente porque coleções fornecem uma infraestrutura completa para a manipulação de seus elementos. Para citar alguns de seus benefícios, podemos destacar: redimensionamento automático; método para inserir, remover e verificar se existe um determinado elemento; métodos que permitem o usuário interceptar a inserção ou a exclusão de um elemento. As coleções disponibilizadas pelo .NET Framework 2.0 e as Interfaces que são base para grande parte delas estão atualmente disponibilizadas em dois namespaces diferentes: System.Collections e System.Collections.Generics. O primeiro já existe desde a versão 1.0 do .NET Framework, mas o segundo é uma novidade que foi incorporada na versão 2.0. Dentro destes namespaces, encontramos várias classes e Interfaces para a criação dos mais diversos tipos de coleções. As coleções estão definidas em três categorias: Coleções Ordenadas, Coleções Indexadas e Coleções baseadas em "chave-e-valor". Veremos abaixo a finalidade de cada uma delas:

Page 53: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 2

Israel Aece | http://www.projetando.net

2

• Coleções Ordenadas: São coleções que são distingüidas apenas pela ordem de

inserção e assim controla a ordem em que os objetos podem ser recuperados da coleção. System.Collections.Stack e System.Collections.Queue são exemplos deste tipo de coleção.

• Coleções Indexadas: São coleções que são distingüidas pelo fato de que seus valores e/ou objetos podem ser recuperados/acessados através de um índice numérico (baseado em 0 (zero)). System.Collections.ArrayList é um exemplo deste tipo de coleção.

• Coleções "Chave-e-Valor": Como o próprio tipo diz, é uma coleção que contém uma chave associado a algum tipo. Seus índices são classificados no valor da chave e podem ser recuperados na ordem classificado pela enumeração. System.Collections.HashTable é um exemplo deste tipo de coleção.

Nota: Para todos os tipos citados como exemplo para a descrição dos tipos de coleções sempre haverá um correspondente dentro do namespace System.Collections.Generics, que trabalhará de forma “genérica”. Coleções Primárias Definimos como coleções primárias todas as coleções disponíveis dentro do namespace System.Collections. Estas coleções também são chamadas de coleções fracamente tipadas ou coleções não genéricas. Essas coleções, apesar de suportar grande partes das funcionalidades que são disponibilizadas pelo .NET Framework, há um grande problema, já que essas classes operam com tipos System.Object, o que causa problemas em termos de performance, pois se quiser fazer uma coleção de inteiros teremos que pagar pelo boxing e unboxing. Além disso, ainda há a questão de não trabalhar efetivamente com um único tipo, ou seja, o fato de possibilitar incluir um System.Object, está permitindo adicionar qualquer tipo de objeto dentro da coleção, o que pode causar problemas durante a execução da aplicação. Para enterdemos a estrutura das coleções primárias, vamos analisar todas as Interfaces que foram implementadas nas coleções concretas e que também estão disponíveis para que o desenvolvedor possa customizar seus próprias coleções. A Imagem 2.1 exibe a hierarquia das Interfaces e a tabela a seguir detalha cada uma dessas Interfaces.

Page 54: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 3

Israel Aece | http://www.projetando.net

3

Imagem 2.1 – Hierarquia das Interfaces do namespace System.Collections

Interface Descrição ICollection É a Interface base para todas as coleções que estão contidas

dentro do namespace System.Collections. Direta ou indiretamente essas classes a implementa. Ela por sua vez é composta por três propriedades (Count, IsSynchronized e SyncRoot) e um método (CopyTo). Interfaces como IList e IDictionary derivam desta Interface ICollection, pois estas Interfaces são mais especializadas, tendo também outros métodos a serem definidos além dos quais já

Page 55: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 4

Israel Aece | http://www.projetando.net

4

estão contidos dentro da Interface ICollection. Já as classes como System.Collections.Queue e System.Collections.Stack implementam a Interface ICollection diretamente.

IComparer Esta Interface é utilizada para customizar uma ordenação (sorting) em uma determinada coleção. A Interface IComparer é composta por um método chamado Compare, qual compara dois objetos e retorna um valor inteiro indicando se os objetos são ou não iguais, ou seja, se os valores comparados forem iguais, 0 (zero) é retornado. Há classes, como por exemplo o ArrayList, que já implementa o método Sort para tal ordenação, mas há casos em que desejamos criar uma ordenação customizada, onde a ordenação padrão não nos interessa. É justamente neste momento que a Interface IComparer é utilizada, inclusive o método Sort da classe ArrayList tem um overload que recebe como parâmetro um objeto do tipo Interface IComparer. Quando utilizamos o método Sort do ArrayList sem informar nenhum parâmetro, a ordenação é feita em ordem ascendente, a padrão. Mas podemos querer ordenar em ordem descendente, e com isso teríamos que criar uma classe que implementa a Interface IComparer, codificando assim o método Compare.

IDictionary A Interface IDictionary é a base para todas as coleções do tipo "chave-e-valor". Cada elemento inserido neste tipo de coleção é armazenado em um objeto do tipo DictionaryEntry (Structure). Cada associação deve ter um chave original que não seja uma referência nula, mas o seu respectivo valor de associação pode incluir sem problemas uma referência nula.

IDictionaryEnumerator Esta Interface tem a mesma finalidade da Interface IEnumerator, ou seja, percorrer e ler os elementos de uma determinada coleção, mas esta em especial, trata de enumerar os elementos de um dicionário, ou seja, uma coleção que contenha "chave-e-valor".

IEnumerable É a base direta ou indiretamente para algumas outras Interfaces e todas as coleções a implementam. Esta interface tem apenas um método a ser implementado, chamado GetEnumerator, qual retorna um objeto do tipo da Interface de IEnumerator.

IEnumerator É a base para todos os enumeradores. Composta por uma propriedade (Current) e dois métodos (MoveNext e Reset), percorre e lê todos os elementos de uma determinada coleção, permitindo apenas ler os dados, não podendo alterá-los. Vale levar em consideração que enumeradores não podem ser

Page 56: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 5

Israel Aece | http://www.projetando.net

5

utilizados para a coleção subjacente. IEqualityComparer Expõe os métodos chamados Equals e GetHashCode que

permitem a comparação entre dois objetos quando a igualdade. Esta Interface foi introduzida a partir do .NET Framework 2.0.

IList Derivada das Interfaces ICollection e IEnumerable ela é a Interface base para todas as listas contidas no .NET Framework. Listas quais podemos acessar individualmente por um determinado índice. As implementações da Interface IList podem estar em uma das seguintes categorias: Read-Only, Fixed-Size e Variable-Size. Uma IList Read-Only, não permite que você modifique os elementos. Já uma IList Fixed-Size, não permite adicionar ou remover os elementos, mas permite que você altere os elementos existentes. E por fim, uma IList Variable-Size, qual permite que você adicione, remova ou altere os elementos. Claro que quando implementamos esta Interface em alguma classe, é necessário criarmos um membro privado que será o responsável por armazenar os items. Este membro privado é manipulado pelos métodos e propriedades da Interface IList que serão implementados nesta classe.

Antes de efetivamente entrar em cada uma das coleções concretas dentro das coleções primárias, vamos analisar a implementação de alguma das Interfaces que vimos acima para um cenário customizado de uma determinada aplicação. Entre as várias Interfaces acima mostradas, duas delas praticamente trabalham em conjunto: IEnumerable e IEnumerator. A Interface IEnumerator fornece métodos para iterar pela coleção; já a Interface IEnumerable deve ser implementada em um tipo que você deseja iterar pela coleção através de um loop foreach (For Each em Visual Basic .NET). A implementação destas Interfaces é geralmente feito em classes separadas e, na maioria dos casos, o método GetEnumerator da Interface IEnumerable retorna a instância da classe privada que implementa IEnumerator. O código abaixo exemplifica como iterar pelas cidades categorias contidas dentro da classe CategoriesEnumerator: VB.NET Public Class CategoriesEnumerator Implements IEnumerator Private _categories() As String = _ {"ASP.NET", "VB.NET", "C#", "SQL", "XML"} Private _current As Integer = -1

Page 57: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 6

Israel Aece | http://www.projetando.net

6

Public ReadOnly Property Current() As Object Implements IEnumerator.Current Get If Me._current < 0 OrElse Me._current > 4 Then Throw New InvalidOperationException Else Return Me._categories(Me._current) End If End Get End Property Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext Me._current += 1 Return Me._current <= 4 End Function Public Sub Reset() Implements IEnumerator.Reset Me._current = -1 End Sub End Class C# public class CategoriesEnumerator : IEnumerator { private string[] _categories = new string[] { "ASP.NET", "VB.NET", "C#", "SQL", "XML" }; private int _current = -1; public object Current { get { if (this._current < 0 || this._current > 4) throw new InvalidOperationException(); else return this._categories[this._current]; } } public bool MoveNext() { this._current++; return this._current <= 4; } public void Reset() {

Page 58: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 7

Israel Aece | http://www.projetando.net

7

this._current = -1; } } A classe acima está finalizada. Se quiséssemos exibir os valores na tela, bastaria loop verificando se o método retorna True ou False e exibir o conteúdo da tela recuperand-o através da propriedade Current. Mas e se quiséssemos iterar por essa coleção através de um laço For Each? É nesta ocasião que a Interface IEnumerable entra em ação. É necessário que você cria uma classe auxiliar, implementando a Interface em questão onde, dentro do método GetEnumerator fornecido por ela, retornará uma instância da classe CategoriesEnumerator. O código abaixo mostra essa implementação na íntegra e a respectiva utilização: VB.NET Public Class Categories Implements IEnumerable Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator Return New CategoriesEnumerator() End Sub End Class ‘Utilização: For Each category As String In New Categories() Console.WriteLine(category) Next C# public class Categories : IEnumerable { public IEnumerator GetEnumerator() { return new CategoriesEnumerator(); } } //Utilização: foreach (string category in new Categories()) Console.WriteLine(category);

Page 59: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 8

Israel Aece | http://www.projetando.net

8

Outra Interface que é comumente utilizada é a IList. Ela fornece grande parte da infraestrutura para todas as coleções do .NET Framework. Um exemplo de implementação para esta Interface é exibido abaixo: VB.NET Public Class TestIList Implements IList Private _arr As ArrayList Public Sub New() Me._arr = New ArrayList End Sub Public ReadOnly Property Count() As Integer Implements System.Collections.ICollection.Count Get Return Me._arr.Count() End Get End Property Public Function Add(ByVal value As Object) As Integer Implements System.Collections.IList.Add Return Me._arr.Add(value) End Function ' outros métodos ocultados End Class C# public class TestIList : IList { private ArrayList _arr; public TestIList() { this._arr = new ArrayList(); } public int Count { get { return this._arr.Count; } } public int Add(object value)

Page 60: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 9

Israel Aece | http://www.projetando.net

9

{ return this._arr.Add(value); } } É importante ressaltar que no código acima é criado um membro privado chamado de _arr do tipo ArrayList e que sua manipulação é feita através dos métodos que estão definidos na Interface IList e nesta classe implementados. Com a Interface IList conseguimos criar uma coleção customizada, podendo inclusive criar uma coleção fortemente tipada, onde seus tipos seriam únicos, ou seja, seria aceito apenas um objeto de um tipo definido e assim, em design-time já conseguiríamos detectar possíveis problemas de casting, mas infelizmente não evita o boxing e unboxing. O código acima é apenas um exemplo para entedermos o funcionamento da implementação da Interface IList, onde a implementamos e criamos um membro privado do tipo ArrayList para armazenar os objetos, pois dentro do namespace System.Collections já temos um classe abstrata, chamada CollectionBase, que é a base para a criação de coleções fortemente tipadas. Veremos mais tarde, ainda neste capítulo, sobre a coleção CollectionBase. Coleções ArrayList A coleção ArrayList é um dos objetos mais tradicionais que o .NET Framework expõe. Trata-se de uma classe que implementa a Interface IList e é muito similar a um Array unidemensional, mas automaticamente redimesiona o seu tamanho (que inicialmente é 0) de acordo com a necessidade. Através do método Add, podemos adicionar qualquer tipo de objeto, sendo uma referência nula, um objeto já adicionado anteriormente ou de tipos diferentes. Depois de adicionados, seus items podem ser acessados através de um índice inteiro (propriedade (indexer) fornecido pela Interface IList), que indica a posição do elemento que quer recuperar. Trata-se de uma coleção baseada em 0 (zero) e, sendo assim, a posição do primeiro elemento é 0. Para exibir um exemplo do uso da classe ArrayList é um tanto quanto simples: VB.NET Dim categorias As New ArrayList() categorias.Add(“ASP.NET”) categorias.Add(“VB.NET”) categorias.Add(“C#”)

Page 61: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 10

Israel Aece | http://www.projetando.net

10

categorias.Add(“XML”) ‘Utilização: For index As Integer = 0 To categorias.Count – 1 Console.WriteLine(categorias(index)) Next C# ArrayList categorias = new ArrayList(); categorias.Add(“ASP.NET”); categorias.Add(“VB.NET”); categorias.Add(“C#”); categorias.Add(“XML”); for (int index = 0; index < categorias.Count; index++) Console.WriteLine(categorias[index]); Stack A coleção Stack é uma coleção do tipo LIFO (last-in-first-out), ou seja, o primeiro item a entrar na coleção é o último a sair. Ela oferece dois métodos importantes: Push e Pop. O primeiro encarrega-se de adicionar o item a coleção. Já o segundo, Pop, é utilizado para recuperar e remover o último item adicionado na coleção. Além destes dois métodos, ainda existe o método Peek, que trabalha basicamente da mesma forma que o método Pop, ou seja, recupera o último item adicionado, mas com uma grande diferença: não remove o item da coleção. Para exibir um exemplo da utilização da coleção Stack, analise o código abaixo: VB.NET Dim stack As New Stack() stack.Push("1") stack.Push("2") stack.Push("3") stack.Push("4") Console.WriteLine(“Quantidade: “ & stack.Count) While(stack.Count > 0) Console.WriteLine(stack.Pop()); End While Console.WriteLine(“Quantidade: “ & stack.Count) ‘Output: Quantidade: 4 4

Page 62: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 11

Israel Aece | http://www.projetando.net

11

3 2 1 Quantidade: 0 C# Stack stack = new Stack(); stack.Push("1"); stack.Push("2"); stack.Push("3"); stack.Push("4"); Console.WriteLine(“Quantidade: “ + stack.Count); while (stack.Count > 0) Console.WriteLine(stack.Pop()); Console.WriteLine(“Quantidade: “ + stack.Count); //Output: Quantidade: 4 4 3 2 1 Quantidade: 0 Queue A coleção Queue trata-se de uma coleção do tipo FIFO (first-in-first-out), ou seja, o primeiro a entrar na lista é o primeiro a sair. Há alguns programas que trabalham neste formato, como por exemplo o Message Queue do Windows. Esse tipo de coleções são úteis quando necessitamos trabalhar em um ambiente que exige um processamento sequencial. Podemos fazer uma analogia a qualquer tipo de fila de qualquer estabelecimento. Exemplo: em um banco, onde somente exista um caixa atendendo uma fila de N clientes, o primeiro a chegar, que obviamente está no início da file, será o primeiro a ser atendido e, conseqüentemente, o primeiro a sair e, o processo continua até o término da fila. Assim como na coleção Stack, a Queue também possui dois métodos importantes: Enqueue e Dequeue. O primeiro encarrega-se de enfileirar o valor a coleção. Já o segundo, Dequeue, é utilizado para recuperar e remover o item mais antigo adicionado na coleção. Novamente temos o método Peek, que trabalha basicamente da mesma forma que o método Dequeue, ou seja, recupera o item mais antigo adicionado, mas com uma grande diferença: não remove o item da coleção. Para exibir um exemplo da utilização da coleção Queue, analise o código abaixo:

Page 63: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 12

Israel Aece | http://www.projetando.net

12

VB.NET Dim queue As New Queue() queue.Enqueue("1") queue.Enqueue("2") queue.Enqueue("3") queue.Enqueue("4") Console.WriteLine(“Quantidade: “ & queue.Count) While(queue.Count > 0) Console.WriteLine(queue.Dequeue()); End While Console.WriteLine(“Quantidade: “ & queue.Count) ‘Output: Quantidade: 4 4 3 2 1 Quantidade: 0 C# Queue queue = new Queue(); queue.Enqueue("1"); queue.Enqueue("2"); queue.Enqueue("3"); queue.Enqueue("4"); Console.WriteLine(“Quantidade: “ + queue.Count); while (queue.Count > 0) Console.WriteLine(queue.Dequeue()); Console.WriteLine(“Quantidade: “ + queue.Count); //Output: Quantidade: 4 4 3 2 1 Quantidade: 0 Hashtable Esta classe representa uma coleção do tipo chave/valor e são organizadas de acordo com um valor hash da chave informada e armazenada em uma estrutura do tipo

Page 64: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 13

Israel Aece | http://www.projetando.net

13

DictionaryEntry. Como é calculado um valor hash em cima da chave informada, a performance é beneficiada, pois reduz o número de comparações efetuadas quando se procura por uma determinada chave, pois o Hashtable extrai o código hash do valor a ser encontrado e submete este valor para a pesquisa. Certifique-se sempre antes de adicionar um item a coleção de que esta chave já não existe, pois do contrário, uma exceção do tipo ArgumentException é atirada. Como o Hashtable armazena o código hash para a chave informada, não altere este valor, senão o Hashtable não será mais capaz de encontrá-lo. Se a sua chave tende a mudar, então o correto é remover o item da coleção, alterar a chave e, em seguida, adicioná-lo novamente. A chave nem sempre precisa ser uma string. Qualquer objeto que implemente o método System.Object.GetHashCode e System.Object.Equals será permitido. É importante saber que a chave não pode ser uma referência nula, ao contrário do valor, que permite isso. Para exibir um exemplo simples da utilização da coleção Hashtable, analise o código abaixo: VB.NET Dim table As new Hashtable() table.Add("AC", "Acre") table.Add("RJ", "Rio de Janeiro") table.Add("SP", "São Paulo") For Each entry As DictionaryEntry In table Console.WriteLine(entry.Key & " - " & entry.Value) Next C# Hashtable table = new Hashtable(); table.Add("AC", "Acre"); table.Add("RJ", "Rio de Janeiro"); table.Add("SP", "São Paulo"); foreach (DictionaryEntry entry in table) Console.WriteLine(entry.Key + " - " + entry.Value); Como mencionamos anteriormente, os objetos adicionados dentro de uma coleção Hashtable são do tipo DictionaryEntry, o que significa que para iterar entre os valores do mesmo, terá que gerar o laço como exemplicado no código acima. SortedList

Page 65: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 14

Israel Aece | http://www.projetando.net

14

A coleção SortedList é basicamente uma coleção Hashtable, com uma diferença: é uma coleção ordenada pela chave. Evidentemente que a SortedList tende ser mais lenta que a Hashtable, devido ao overhead de sorting. Quando um item é adicionado em uma SortedList, ele já é inserido exatamente no local correto e os índices são ajustados automaticamente. O mesmo acontece quando o elemento é removido da coleção. Entretanto, é importante observar que o índice de um determinado item pode variar, pois a coleção é reordenada e, se tiver um índice para um objeto específico, ele já pode não mais apontar para o mesmo. Para exibir um exemplo simples da utilização da coleção SortedList, analise o código abaixo: VB.NET Dim list As new SortedList() list.Add("RJ", "Rio de Janeiro") list.Add("AC", "Acre") list.Add("SP", "São Paulo") For Each entry As DictionaryEntry In list Console.WriteLine(entry.Key & " - " & entry.Value) Next ‘Output AC – Acre RJ – Rio de Janeiro SP – São Paulo C# SortedList list = new SortedList(); list.Add("RJ", "Rio de Janeiro"); list.Add("AC", "Acre"); list.Add("SP", "São Paulo"); foreach (DictionaryEntry entry in list) Console.WriteLine(entry.Key + " - " + entry.Value); //Output AC – Acre RJ – Rio de Janeiro SP – São Paulo Além dos items da coleção SortedList serem acessíveis através da chave, é também possível acessá-lo através do índice. Para isso, utilize o método GetByIndex passando como parâmetro um número inteiro que corresponde ao elemento que quer recuperar.

Page 66: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 15

Israel Aece | http://www.projetando.net

15

CollectionBase É muito comum em runtime retornarmos valores da Base de Dados e armazenarmos em objetos do tipo Datasets, mas com isso ocorre o “Weakly Typed” e o “Late Binding”, ou seja, não temos a segurança de tipos e o acesso à suas propriedades somente ocorre durante o tempo de execução. Tendo esse problema, o que podemos fazer para termos nossos próprios objetos com suas respectivas propriedades e métodos ao invés de uma genérica? Eis o momento que entra em cena a classe CollectionBase. A classe CollectionBase é uma classe onde podemos apenas herdá-la, ou seja, é uma classe abstrata. Ela implementa as três seguintes Interfaces: IList, ICollection e IEnumerable que definem os métodos que permitem aceitar tipos System.Object. Além disso, ela ainda encapsula um objeto do tipo ArrayList, onde ficarão armazenados os elementos da coleção. Entre os principais métodos desta classe, temos: Método Descrição Add Adiciona um novo elemento na coleção e retorna o indíce (posição) que o

objeto foi adicionado. Contains Verifica se já existe um determinado elemento dentro da coleção e retorna

um valor boleano indicando ou não sua existência. Insert Insere um elemento na coleção em uma determinada posição. Item Retorna ou recebe um elemento dado uma posição. Remove Remove um elemento da coleção. RemoveAt Remove um objeto da coleção dado uma posição. Os métodos acima são expostos através da propriedade List. Será necessário criar esses métodos que servirão de wrapper para os métodos internos de manipulação da coleção. A direferença é que esses métodos que serão criados e expostos para o cliente devem aceitar em seus parâmetros o mesmo tipo, isso quer dizer que, se a coleção trabalhará somente com objetos do tipo Usuario, é necessário que os membros Add, Remove, Contains e Item recebam este mesmo tipo para garantir o type-safe. O trecho de código abaixo mostra como a classe deve ser implementada: VB.NET Public Class UsuarioColecao Inherits CollectionBase Public Function Add(ByVal u As Usuario) As Integer Return MyBase.List.Add(u) End Function Public Function Contains(ByVal u As Usuario) As Boolean Return MyBase.List.Contains(u)

Page 67: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 16

Israel Aece | http://www.projetando.net

16

End Function Public Sub Insert(ByVal index As Integer, _ ByVal u As Usuario) MyBase.List.Insert(index, u) End Sub Default Public Property Item(ByVal index As Integer) As Usuario Get Return CType(MyBase.List(index), Usuario) End Get Set(ByVal Value As Usuario) MyBase.List(index) = Value End Get End Property Public Sub Remove(ByVal u As Usuario) MyBase.List.Remove(u) End Sub End Class C# public class UsuarioColecao : CollectionBase { public int Add(Usuario u) { return base.List.Add(u); } public bool Contains(Usuario u) { return base.List.Contains(u); } public void Insert(int index, Usuario u) { base.List.Insert(index, u); } public Usuario this[int index] { get { return (Usuario)base.List[index]; } set { base.List[index] = value;

Page 68: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 17

Israel Aece | http://www.projetando.net

17

} } public void Remove(Usuario u) { base.List.Remove(u); } } Durante a sua utilização, ela somente poderá manipular objetos do tipo Usuario. Qualquer outro tipo que você tentar adicionar resultará em um erro de compilação e não conseguirá compilar o projeto até que o problema seja sanado. Isso irá resolver o problema do type-safe, já que ao invés de expor objetos do tipo ArrayList, você pode optar por expor a coleção customizada. Apesar de uma boa melhora, ainda temos alguns problemas que nos causam prejuízo: a produtividade é muito baixa, já que, se tivéssemos 20 coleções de objetos diferentes, devemos ter 20 coleções distintas. Além disso, o problema de boxing/unboxing continua existindo, já que internamente a classe CollectionBase armazena em um objeto ArrayList. Esses problemas foram todos abolidos com a introdução dos Generics, que veremos ainda neste capítulo. DictionaryBase Assim como a classe CollectionBase, a DictionaryBase vem para forneceer uma infraestrutura base para a criação de dicionários (pares de chave-valor) com tipos específicos que você desejar, para novamente garantir o type-safe. Internamente esta coleção armazena os itens em uma outra coleções do tipo Hashtable, que já vimos acima. A implementação é basicamente a mesma em relação ao que vimos acima com a coleção CollectionBase, apenas mudando que temos uma coleção do tipo chave-valor que, por sua vez, armazenará uma string como chave e um objeto do tipo Usuario como valor: VB.NET Public Class UsuarioDictionary Inherits DictionaryBase Public Sub Add(ByVal key As String, ByVal value As Usuario) MyBase.Dictionary.Add(key, value) End Function ‘Outros métodos End Class

Page 69: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 18

Israel Aece | http://www.projetando.net

18

C# public class UsuarioDictionary : DictionaryBase { public void Add(string key, Usuario value) { base.Dictionary.Add(key, value); } //Outros métodos } Com relação a forma de iteração, continua sendo a mesma da Hashtable, ou seha, através da estrutura DictionaryEntry. Mais uma vez, isso somente ajudará no que diz respeito ao type-safe. A produtividade e performance ainda continuam comprometidas. Coleções Genéricas Na seção anterior vimos a infraestrutura e a utilização das mais diversas coleções disponíveis no .NET Framework desde a sua versão 1.0. Como também foi dito, temos alguns problemas quando fazemos das coleções primárias, pois havia sempre boxing e unboxing para adicionar, recuperar e remover os elementos de cada uma das coleções. Um outro agravante é que para termos uma coleção “fortemente tipada” tínhamos que escrever uma porção de código para garantir o “type-safe”, mas mesmo assim, não evitava o boxing/unboxing, prejudicando imensamente a performance. Felizmente a Microsoft introduziu as coleções genéricas. Essas coleções são basicamente as mesmas que vimos anteriormente com uma enorme diferença: operam com qualquer tipo definido dentro do .NET Framework ou um tipo customizado pelo desenvolvedor. Além disso, não há mais boxing/unboxing e também não exige mais o casting quando necessitar recuperar um dos elementos da coleção, já que toda a checagem de tipos é efetuada durante a escrita do código, ou seja, se criar uma coleção genérica especificando o tipo string para este coleção, jamais conseguirá adicionar um tipo de dado inteiro ou decimal. Interfaces disponíveis Foi introduzido um novo namespace chamado de System.Collections.Generic. Dentro deste namespace há todas as Interfaces primárias, só que em sua forma genérica. Isso quer dizer que temos todas essas Interfaces agoram operando com um tipo genérica. Basicamente dentro de cada Interface genérica temos o(s) mesmo(s) membro(s) que possuem as Interfaces primárias, só que estes tipos são substituídos pelo tipo que o desenvolvedor desejar manipular.

Page 70: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 19

Israel Aece | http://www.projetando.net

19

Como a maioria das Interfaces tem exatamente a mesma finalidade das Interfaces primárias, apenas operando com um tipo específico, as mesmas serão apenas citadas abaixo sem suas definições, mas sempre com suas correspondentes primárias: Interface Genérica Correspondente Primária ICollection<T> ICollection IComparer<T> IComparer IDictionary<TKey, TValue> IDictionary IEnumerable<T> IEnumerable IEnumerator<T> IEnumerator IEqualityComparer<T> IEqualityComparer IList<T> IList Cada uma destas Interfaces são implementadas nas mais diversas coleções genéricas, quais serão abordadas ainda neste capítulo. É importante dizer que estas Interfaces, com as Interfaces primárias, estão a disposição para que o desenvolvedor possa criar classes (coleções) mais customizadas, de acordo com sua necessidade. Na tabela acima, o “T” deverá ser substituído por um tipo que você desejar que essa Interface manipule. Uma das únicas exceções é que a Interface IDictionary necessita de dois tipos: um para a chave e outro para o valor. Isso irá possibilitar o utilizador da Interface determinar qual será o tipo de dado da chave e o tipo de dado do valor que deverá ser armazenado pela coleção. Para implementar uma dessas Interfaces, devemos utilizar a seguinte sintaxe: VB.NET Imports System.Collections.Generic Public Class ListaUsuarios Implements IList(Of Usuario) ‘... End Class C# using System.Collections.Generic; public class ListUsuarios : IList<Usuario> { //... } Coleções List<T>

Page 71: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 20

Israel Aece | http://www.projetando.net

20

Uma das mais populares coleções fornecidas pelo namespace das coleções genéricas, System.Collections.Generic, é a coleção List<T>. Essa coleção pode ser comparada ao ArrayList, devido as suas finalidades, só que a coleção List<T> permite que você especifique um tipo, que a coleção toda irá manipular, o que garantirará que todas as propriedades e métodos desta classe somente aceitarão objetos do tipo especificado na construção da coleção, ao contrário do ArrayList que, por sua vez, aceita um System.Object, em outras palavras, “qualquer coisa”, gerando todos os problemas que já discutimos acima. Assim como o ArrayList, a coleção List<T> permite acessar seu itens através de um índice, métodos para efetuar buscas, ordenação e a manipulação completa a coleção e seus respectivos itens. Ambas as coleções implementam as Interfaces IList e IList<T>, respectivamente. Entre os vários métodos e propriedades fornecidos pela coleção List<T> podemos citar os mais importantes que, operam sempre com o tipo especificado durante a criação da coleção: Método Descrição Contains Dado um objeto, ele retorna um valor booleano indicando se o mesmo

existe ou não dentro da coleção corrente. IndexOf Dado um objeto, ele faz a busca dentro da coleção corrente e, se o

encontrar, retorna um número inteiro indicando a posição do elemento. Se não for encontrado, -1 é retornado.

Sort Como o próprio nome diz, é utilizado para permitir a ordenação dos itens que estão contidos na coleção. Este método, em um dos seus overloads, aceita uma instância de um objeto que implementa a Interface IComparer<T> que poderá determinar os critérios de ordenação da coleção.

Count Retorna um número inteiro indicando o número de elementos dentro da coleção. Se não existir nenhum elemento, 0 é retornado.

Item Através da propriedade default (indexer em Visual C#), podemos recuperar ou atribuir um valor para um determinado item da coleção.

Clear Quando invocado, remove todos os itens da coleção. Através do código abaixo, podemos analisar como devemos proceder para a utilização da coleção List<T>: VB.NET Imports System.Collections.Generic Dim lista As New List(Of String) lista.Add(“Generics é legal!”) lista.Add(“Visual Basic .NET suporta Generics!”) lista.Add(123) ‘gera um erro

Page 72: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 21

Israel Aece | http://www.projetando.net

21

C# using System.Collections.Generic; List<string> lista = new List<string>(); lista.Add(“Generics é legal!”); lista.Add(“Visual C# suporta Generics!”); lista.Add(123); // gera um erro Nota Importante: Como a coleção List<T> é otimizada apenas para execução de algumas tarefas onde ter uma boa performance é crucial. Sendo assim, você nunca deve retornar uma coleção do tipo List<T> em suas API’s de object models e sim uma coleção do tipo Collection<T>. A coleção do tipo List<T> não permite que você receba notificações quando o cliente modificar a coleção. Collection<T> A classe/coleção Collection<T> está localizada dentro do namespace System.Collections.ObjectModel. Ela fornece toda a base para a criação de uma coleção genérica e, apesar de não estar em um namespace “dentro” de Generic, ela é uma coleção genérica da mesma forma. Para utilização dela, você pode criar diretamente a instância dela, definindo o tipo que ela irá operar ou, se desejar, ter um tipo específico que representará a coleção. Neste segundo caso, você pode herdar diretamente de Collection<T> e já especificar o tipo. Assim, você poderá expor essa coleção aos clientes e ainda, como já foi falado acima, a coleção Collection<T> é flexível, pois permite interceptarmos a inserção ou remoção de algum elemento da coleção, limpeza, definição de algum elemento mas, para isso, será mesmo necessário herdar em sua classe (coleção) e sobrescrever os métodos que permitem isso, como por exemplo o método RemoveItem. Como existem duas formas de utilizarmos essa coleção, vamos analisar os dois tipos, sendo o primeiro a utilização direta da coleção Collection<T> e, em seguida, essa mesma classe será herdada em uma coleção mais customizada, onde poderemos interceptar algumas operações sob ela. VB.NET Imports System.Collections.ObjectModel Dim coll As New Collection(Of String) coll.Add(“Generics é legal!”) coll.Add(“Visual Basic .NET suporta Generics!”) C#

Page 73: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 22

Israel Aece | http://www.projetando.net

22

using System.Collections.ObjectModel; Collection<string> coll = new Collection<string>(); coll.Add(“Generics é legal!”); coll.Add(“Visual C# suporta Generics!”); VB.NET Imports System.Collections.ObjectModel Module Module1 Sub Main() Dim usuarios As New ColecaoUsuario() AddHandler usuarios.Removed, AddressOf Removido usuarios.Add("José") usuarios.Add("João") usuarios.Add("Augusto") usuarios.Add("Israel") usuarios.Add("Marcelo") usuarios.Add("Claudia") usuarios.Add("Leandro") usuarios.Remove("Israel") End Sub Private Sub Removido(ByVal sender As Object, ByVal e As RemovedItemEventArgs) Console.WriteLine("Item removido: " + e.RemovedItem) End Sub End Module Public Class ColecaoUsuario Inherits Collection(Of String) Public Event Removed As EventHandler(Of RemovedItemEventArgs) Protected Overrides Sub RemoveItem(ByVal index As Integer) Dim removedItem As String = Me(index) MyBase.RemoveItem(index) Dim args As New RemovedItemEventArgs(removedItem) RaiseEvent Removed(Me, args) End Sub End Class Public Class RemovedItemEventArgs Inherits EventArgs Private _removedItem As String

Page 74: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 23

Israel Aece | http://www.projetando.net

23

Public Sub New(ByVal removedItem As String) Me._removedItem = removedItem End Sub Public ReadOnly Property RemovedItem() As String Get Return Me._removedItem End Get End Property End Class C# using System.Collections.ObjectModel; class Program { static void Main(string[] args) { ColecaoUsuario usuarios = new ColecaoUsuario(); usuarios.Removed += new EventHandler<RemovedItemEventArgs>(Removido); usuarios.Add("José"); usuarios.Add("João"); usuarios.Add("Augusto"); usuarios.Add("Israel"); usuarios.Add("Marcelo"); usuarios.Add("Flavia"); usuarios.Add("Leandro"); usuarios.Remove("Israel"); } static void Removido(object sender, Program.RemovedItemEventArgs e) { Console.WriteLine("Item removido: " + e.RemovedItem); } public class ColecaoUsuario : Collection<string> { public event EventHandler<RemovedItemEventArgs> Removed; protected override void RemoveItem(int index) { string removedItem = this.Items[index]; base.RemoveItem(index); if (Removed != null) {

Page 75: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 24

Israel Aece | http://www.projetando.net

24

RemovedItemEventArgs args = new RemovedItemEventArgs(removedItem); this.Removed(this, args); } } } public class RemovedItemEventArgs : EventArgs { private string _removedItem; public RemovedItemEventArgs(string removedItem) { this._removedItem = removedItem; } public string RemovedItem { get { return this._removedItem; } } } } Como podemos analisar neste segundo caso, foi criado uma classe chamada ColecaoUsuario que herda diretamente da classe Collection<T>, especificando o tipo string. Com isso, temos acesso a todos os métodos fornecidos pela classe base. A idéia é que quando um elemento for removido da coleção, uma mensagem seja exibida ao usuário que o respectivo item foi removido. Para isso, iremos sobrescrever o método RemoveItem, que é denotado como protected virtual na classe base. Isso irá permitir interceptar o processo de remoção e, via eventos, podemos notificar o cliente que a coleção foi alterada, algo que não é possível fazer com a classe List<T>. Stack<T> e Queue<T> Ambas as coleções tem exatamente a mesma finalidade que as coleções primárias Stack e Queue. A única diferença entre elas é que as classes Stack<T> e Queue<T> operam da forma genérica. Isso quer dizer que, seus principais métodos: Push e Pop, Enqueue e Dequeue, respectivamente, inserem ou retornam sempre o tipo especificado durante a criação da coleção. Em relação as coleções primária, na delcaração precisamos especificar o tipo, asim como já é necessário com a classe List<T>. VB.NET Imports System.Collections.Generic

Page 76: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 25

Israel Aece | http://www.projetando.net

25

Dim coll As New Stack(Of String) C# using System.Collections.Generic; Stack<string> coll = new Stack<string>(); Dictionary<TKey, TValue> Essa coleção trata-se da versão genérica da classe Hashtable. Todas as operações fornecidas por esta classe baseiam-se nos tipos especificados em sua declaração. Esta classe fornece em sua construção dois tipos que devemos informar: TKey e TValue. O primeiro representa o tipo que será a chave e o segundo representará o tipo que será o objeto que devemos aceitar como valor, associado a chave. Assim como o Hashtable, a chave não pode ser uma referência nula, mas o valor pode ser, sem problema algum. Ainda há uma mudança com relação ao objeto que representará a chave e o valor durante a iteração da coleção. Ao invés de utilizarmos a estrutura DictionaryEntry, estaremos utilizando a estrutura KeyValuePair<TKey, TValue> que é a versão genérica da estrutura anterior e será utilizado por todas as coleções genéricas que operam utilizando chave e valor. Essa coleção utiliza uma implementação da Interface IEqualityComparer para determinar se as chaves são ou não iguais. Para isso, você pode especificar essa implementação customizada através do construtor da classe Dictionary. O código abaixo exibe uma uma solução onde a chave deve ser uma string e o valor um objeto do tipo Usuario. Depois de adicionar os itens e suas respectivas chaves, ele exibe os mesmos através de um laço For Each. Se reparar, logo após a propriedade Key ou Value da estrutura KeyValuePair, já podemos acessar qualquer membro dos objetos, como é o caso da propriedade Nome da classe Usuario: VB.NET Imports System.Collections.Generic Dim dic As New Dictionary(Of String, Usuario) dic.Add("JT", New Usuario("Jose Torres")) dic.Add("ZC", New Usuario("Zuleika Camargo")) dic.Add("MA", New Usuario("Maria Antonieta")) For Each pair As KeyValuePair(Of String, Usuario) In dic Console.WriteLine(pair.Value.Nome) Next

Page 77: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 26

Israel Aece | http://www.projetando.net

26

C# using System.Collections.Generic; Dictionary<string, Usuario> dic = new Dictionary<string, Usuario>(); dic.Add("JT", new Usuario("Jose Torres")); dic.Add("ZC", new Usuario("Zuleika Camargo")); dic.Add("MA", new Usuario("Maria Antonieta")); foreach (KeyValuePair<string, Usuario> pair in dic) { Console.WriteLine(pair.Value.Nome); } SortedList<TKey, TValue> e SortedDictionary<TKey, TValue> Novamente, a SortedList<TKey, TValue> e a versão genérica para a classe SortedList, que opera com tipos genéricos. Essa classe recebe em seu construtor uma implementação da Interface IComparer para determinar se as chaves são ou não iguais. A SortedDictionary<TKey, TValue> trata-se de uma nova coleção que foi introduzida na versão 2.0 do .NET Framework, que também trabalha como uma coleção de chave e valor, baseando-se em uma única chave. Assim como a SortedList, é também ordenada pela chave, mas com uma importante diferença: é muito mais rápido em comparada a SortedList<TKey, TValue> em relação a inserção e remoção de elementos, mas utiliza mais memória que ela. A utilização destas classes são bem semelhantes. O que realmente muda é o comportamente interno de cada uma delas. VB.NET Imports System.Collections.Generic Dim sl As New SortedList(Of String, Usuario) Dim sd As New SortedDictionary(Of String, Usuario) sl.Add("MA", New Usuario("Maria Antonieta")) sd.Add("MA", New Usuario("Maria Antonieta")) C# using System.Collections.Generic; SortedList<string, Usuario> sl = new SortedList<string, Usuario> (); SortedDictionary<string, Usuario> sd = new

Page 78: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 27

Israel Aece | http://www.projetando.net

27

SortedDictionary<string, Usuario> (); sl.Add("MA", new Usuario("Maria Antonieta")); sd.Add("MA", new Usuario("Maria Antonieta")); LinkedList<T> Imagine que temos uma lista de tarefas a serem realizadas em um determinado projeto. Essas tarefas são melhorias, validações e também alguns ajustes que devem ser realizados para que o projeto continue funcionando. Você e sua equipe vão desenvolvem a lista com todas essas tarefas e, neste momento, não se preocupam com a propriedade de cada uma delas. Como tal projeto já está em produção e os clientes estão a todo vapor o utilizando, as melhorias devem ser as últimas a serem realizadas. Já as validações e os ajustes devem ter uma prioriodade maior sobre as melhorias. Neste cenário, como é que podemos adicionar os ajustes e validações acima das melhorias, que não são uma necessidade no momento? Transformando todo esse cenário em .NET, mais especificamente em uma coleção, essas tarefas, ainda sem uma ordem específica, foram sendo adicionadas à uma coleção. Agora é necessário adicionar as tarefas especificando um nó (tarefa) que é representado pelo elemento da coleção e assim, podemos inserir a nova tarefa antes ou depois do nó especificado durante a inserção do novo elemento. A coleção que nos oferece essa estrutura é a LinkedList<T>. Ela permite inserir um elemento na coleção antes ou depois de elemento já existente que é especificado durante a inserção ou remoção. Cada um dos elementos que são inseridos dentro desta coleção são encapsulados dentro de uma classe do tipo LinkedListNode<T>. Como trata-se de uma coleção do tipo “linked”, a classe LinkedListNode<T> fornece duas propriedades bastante interessantes, chamadas: Previous e Next que retornam também um objeto do tipo LinkedListNode<T>. O primeiro, Previous, retorna uma referência para o nó anterior e nulo se estiver sendo chamado através do primeiro nó da coleção. Já o método Next, retorna uma referência para o nó seguinte, também retornando nulo se a propriedade estiver sendo chamada através do último nó da coleção. Já entre os membros da classe LinkedList<T>, temos alguns que merecem serem citados: Membro Descrição First Esta propriedade retorna um objeto do tipo LinkedListNode<T> que

representa o primeiro elemento da coleção. Se a coleção estiver vazia, será retornado um valor nulo.

Last Esta propriedade retorna um objeto do tipo LinkedListNode<T> que representa o último elemento da coleção. Se a coleção estiver vazia, será retornado um valor nulo.

Page 79: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 28

Israel Aece | http://www.projetando.net

28

AddAfter Adiciona um novo nó com o novo valor, logo após o nó especificado. AddBeforer Adiciona um novo nó com o novo valor, antes do nó especificado. AddFirst Adiciona um novo nó no início da coleção. AddLast Adiciona um novo nó no final da coleção. Find Dado um valor, retorna a um objeto do tipo LinkedListNode<T> que

representa o nó em que o objeto especificado está encapsulado. Se a coleção não encontrar o valor especificado, uma referência nula é retornada.

RemoveFirst Remove o primeiro nó da coleção. RemoveLast Remove o último nó da coleção. Para exemplificar, será criado uma coleão LinkedList<T> onde teremos as tarefas de um determinado projeto. Teremos três tarefas de categorias diferentes: melhoria, ajuste e erro. Como os erros são prioridades, devemos sempre colocá-los em uma sequência, já que os erros são sempre críticos e tem uma maior prioridade em relação a qualquer outra tarefa. Sendo assim, analise o código abaixo: VB.NET Imports System.Collections.Generic Dim projeto As New LinkedList(Of String) projeto.AddFirst("[Melhoria]: Melhoria 1.") projeto.AddFirst("[Ajuste]: Ajuste 1.") projeto.AddFirst("[Erro]: Erro 1.") Dim erro1 As LinkedListNode(Of String) = projeto.Find("[Erro]: Erro 1.") If Not IsNothing(erro1) Then projeto.AddAfter(erro1, "[Erro]: Erro 2.") End If For Each node As String In projeto Console.WriteLine(node) Next C# using System.Collections.Generic; LinkedList<string> projeto = new LinkedList<string>(); projeto.AddFirst("[Melhoria]: Melhoria 1."); projeto.AddFirst("[Ajuste]: Ajuste 1."); projeto.AddFirst("[Erro]: Erro 1."); LinkedListNode<string> erro1 = projeto.Find("[Erro]: Erro 1.");

Page 80: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 29

Israel Aece | http://www.projetando.net

29

if(erro1 != null) projeto.AddAfter(erro1, "[Erro]: Erro 2."); foreach (string node in projeto) Console.WriteLine(node); Inicialmente criamos a instância da coleção LinkedList especificando o tipo string. Em seguida, adicionamos três itens (tarefas) através do método AddFirst. Como este método adiciona o item sempre em primeiro lugar na lista, neste caso, o último será o primeiro. Como no nosso cenário é importante termos os erros em primeiro lugar, ao receber um novo erro que deve ser colocado no lista, precisamos recuperar o último erro inserido através do método Find e, se encontrado na coleção, devemos invocar o método AddAfter para adicionar essa nova tarefa imediatamente abaixo do última tarefa de erro criada. Para ambos os códigos, o retorno será o mesmo: [Erro]: Erro 1. [Erro]: Erro 2. [Ajuste]: Ajuste 1. [Melhoria]: Melhoria 1. Iterators Os Iterators é uma nova funcionalidade disponibilizada apenas pelo Visual C# 2.0. Logo no início deste capítulo, em Coleções Primárias, vimos que para que possamos iterar entre os elementos de uma coleção customizada através de um laço foreach (For Each em Visual Basic .NET) é necessário implementarmos a Interface IEnumerable. Esta Interface IEnumerable expõe um método chamado GetEnumerator que retorna um enumerador (classe que implementa a Interface IEnumerator) que, por sua vez, fornecem os seguintes membros: MoveNext, Current, Reset e Dispose, quais são utilizados pelo runtime para que ele possa recuperar cada item da coleção. Agora, com os Iterators, isso não é mais necessário, já que quando o compilador detecta a presença de um Iterator, ele automaticamente gera o código necessário para a criação do enumerador. Com isso, não será mais necessário criarmos uma classe, geralmente uma classe privada, que implemente a Interface IEnumerator que é retornada através do método GetEnumerator. Para exemplificar o uso de um Iterator, podemos utilizar o mesmo exemplo que utilizamos quando foi abordado sobre as Interfaces IEnumerable e IEnumerator. A idéia é iterar entre as categorias definidas no interior de uma classe qualquer:

Page 81: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 30

Israel Aece | http://www.projetando.net

30

C# public class Categories : IEnumerable { private string[] _categories = new string[ ] { "ASP.NET", "VB.NET", "C#", "SQL", "XML" }; public IEnumerator GetEnumerator() { for (int i = 0; i < this._categories.Length; i++) { yield return this._categories[i]; } } } //Utilização: foreach (string category in new Categories()) Console.WriteLine(category); Apesar de não mais precisarmos de uma classe auxiliar para criar o enumerador, quando utilizamos a keyword yield, automaticamente o compilar gera a classe que implementa a Interface IEnumerator, implementando todos os métodos necessários para a iteração entre os elementos da coleção. A keyword yield que determina a criação do enumerador, ainda permite uma outra forma de passar os itens para o mesmo, mantendo o mesmo resultado final. O trecho de código abaixo é mostrado essa outra forma da utilização do Iterator: C# public class Categories : IEnumerable { public IEnumerator GetEnumerator() { yield return "ASP.NET"; yield return "VB.NET"; yield return "C#"; yield return "SQL"; yield return "XML"; } } Nota: Apesar de que o exemplo está sendo mostrado com a Interface primária IEnumerable, você pode também implementar os Iterators com a Interface genérica IEnumerable<T>.

Page 82: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 31

Israel Aece | http://www.projetando.net

31

Coleções Especializadas Como o próprio nome diz, as coleções especializadas são coleções criadas especificamente para um propósito muito especial. A Microsoft disponibilizou várias delas que estão contidas dentro do namespace System.Collections.Specialized. Essas classes foram criadas para operarem sempre com um determinado tipo, justamente para conseguir adaptar e otimizar o melhor possível a coleção, sem overheads extras. As coleções especializadas fornecidas pelo .NET Framework estão divididas em quatro categorias, quais podemos analisar através da tabela abaixo: Categoria Descrição Strings Classes São coleções que foram criadas exclusivamente para

manipularem strings: StringCollection, StringDictionary, StringEnumerator e CollectionsUtil.

Dictionary Classes Coleções contidas dentro desta categoria, fornecem dicionários de dados de algo performance que, dependendo do que armazenam e da quantidade de elementos, mudam o seu comportamento interno para otimizar a leitura, gravação e o armazenamento dos dados. Entre as coleções desta categoria temos: ListDictionary, HybridDictionary e OrderedDictionary.

Named Collection Classes As classes disponibilizadas aqui fornecem uma base para uma coleção que chaves strings e valores objects que você pode acessar através da chave ou de um índice. Isso tipo de coleção permitirá que adicionemos valores duplicados, agrupando por uma chave. Como exemplo destas classes temos: NameValueCollection e NameObjectCollectionBase.

Bit Structures Fornece uma estrutura para armazenar valores booleanos e pequenos inteiros em 32-bits de memória. Para isso temos as seguintes estruturas: BitVector32 e BitVector32.Section.

Cada uma das categorias e suas respectivas coleções estarão detalhadamente explicadas nas próximas páginas. Coleções Specialized String Classes

Page 83: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 32

Israel Aece | http://www.projetando.net

32

StringCollection Esta coleção é basicamente análoga a uma coleção do tipo ArrayList em termos de funcionalidades. A única diferença é que a classe StringCollection trabalha apenas com strings. Internamente, a classe StringCollection armazena os valores em um objeto do tipo ArrayList e, serve de wrapper para o mesmo, fornecendo ao cliente todos os métodos necessários para manipular os elementos dentro dele contidos. A sua implementação é tão simples quanto a do ArrayList: VB.NET Imports System.Collections.Specialized Dim categorias As New StringCollection() categorias.Add(“ASP.NET”) categorias.Add(“VB.NET”) categorias.Add(“C#”) categorias.Add(“XML”) ‘Utilização: For index As Integer = 0 To categorias.Count – 1 Console.WriteLine(categorias(index)) Next C# using System.Collections.Specialized; StringCollection categorias = new StringCollection(); categorias.Add(“ASP.NET”); categorias.Add(“VB.NET”); categorias.Add(“C#”); categorias.Add(“XML”); for (int index = 0; index < categorias.Count; index++) Console.WriteLine(categorias[index]); StringDictionary Esta coleção é basicamente análoga a uma coleção do tipo Hashtable em termos de funcionalidades. A única diferença é que a classe StringDictionary trabalha apenas com strings tanto na chave quanto no valor, podendo apenas o valor ser uma referência nula. Internamente, a classe StringDictionary armazena os valores em um objeto do tipo Hashtable e, serve de wrapper para o mesmo, fornecendo ao cliente todos os métodos necessários para manipular os elementos dentro dele contidos.

Page 84: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 33

Israel Aece | http://www.projetando.net

33

Um pequena diferença é que, ao contrário da classe/coleção Hashtable, esta coleção trabalha independentemente de maiúsculas e minúsculas, fazendo com que as chaves sejam armazenadas em lowercase. A sua implementação é tão simples quanto a do Hashtable: VB.NET Imports System.Collections.Specialized Dim dic As new StringDictionary() dic.Add("AC", "Acre") dic.Add("RJ", "Rio de Janeiro") dic.Add("SP", "São Paulo") For Each entry As DictionaryEntry In dic Console.WriteLine(entry.Key & " - " & entry.Value) Next C# using System.Collections.Specialized; StringDictionary dic = new StringDictionary(); dic.Add("AC", "Acre"); dic.Add("RJ", "Rio de Janeiro"); dic.Add("SP", "São Paulo"); foreach (DictionaryEntry entry in dic) Console.WriteLine(entry.Key + " - " + entry.Value); StringEnumerator Como já vimos acima, os laços foreach (For Each em Visual Basic .NET) ocultam toda a complexidade dos enumeradores, e além disso, é recomendado pela Microsoft ao invés de manipular diretamente o enumerador. Para saber mais sobre os enumeradores, consulta a seção das coleções primárias onde são abordadas as Interfaces IEnumerable e IEnumerator. A classe StringEnumerator é um enumerador que manipula uma coleção de strings. Com isso, o método GetEnumerator da classe StringCollection retorna um enumerador do tipo StringEnumerator, qual utilizaremos para iterar entre os elementos da coleção. O código abaixo exibe um exemplo de como extrair o enumerador de uma StringCoolection: VB.NET Imports System.Collections.Specialized

Page 85: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 34

Israel Aece | http://www.projetando.net

34

Dim cats As New StringCollection() cats.AddRange(New String() {"ASP.NET", "VB.NET", "C#", "SQL", "XML"}) Dim enumerador As StringEnumerator = cats.GetEnumerator() While (enumerador.MoveNext()) Console.WriteLine(enumerador.Current) End While C# using System.Collections.Specialized; StringCollection cats = new StringCollection(); cats.AddRange(new string[] { "ASP.NET", "VB.NET", "C#", "SQL", "XML" }); StringEnumerator enumerador = cats.GetEnumerator(); while (enumerador.MoveNext()) Console.WriteLine(enumerador.Current); CollectionsUtil Esta classe fornece dois métodos estáticos para a criação de coleções que ignoram a diferenciação entre maiúsculas e minúsculas. Ela fornece dois que auxiliam na criação das coleções: CreateCaseInsensitiveHashtable e CreateCaseInsensitiveSortedList. O primeiro deles, retorna uma instância de um Hashtable que não fará a distinção entre maiúsculas e minúsculas, o que significa que a chave “SP” e “sp” são iguais. Já o segundo método, CreateCaseInsensitiveSortedList, tem a mesma finalidade, só que retorna um objeto do tipo SortedList que ignora também maiúsculas e minúsculas. O código abaixo exibe a criação das coleções acima citadas através dos métodos estáticos fornecidos pela classe CollectionsUtil: VB.NET Imports System.Collections.Specialized Dim t As Hashtable = _ CollectionsUtil.CreateCaseInsensitiveHashtable() Dim s As SortedList = _ CollectionsUtil.CreateCaseInsensitiveSortedList() C# using System.Collections.Specialized; Hashtable hashTable = CollectionsUtil.CreateCaseInsensitiveHashtable();

Page 86: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 35

Israel Aece | http://www.projetando.net

35

SortedList sortedList = CollectionsUtil.CreateCaseInsensitiveSortedList(); Uma outra alternativa em relação a classe CollectionsUtil é passar como parâmetro para as classes Hashtable ou SortedList uma instância da classe StringComparer que foi implementanda para ignorar a diferenciação entre maiúsculas e minúsculas. Um dos overloads do construtor dessas classes recebem um tipo IEqualityComparer, onde você pode informar a implementação que é fornecida através do método estático CurrentCultureIgnoreCase da classe StringComparer. A alternativa é exibida através do código abaixo: VB.NET Imports System.Collections.Specialized Dim t As Hashtable = _ New Hashtable(StringComparer.CurrentCultureIgnoreCase) Dim s As SortedList = _ New SortedList(StringComparer.CurrentCultureIgnoreCase) C# using System.Collections.Specialized; Hashtable hashTable = new Hashtable(StringComparer.CurrentCultureIgnoreCase); SortedList lisortedListst = new SortedList(StringComparer.CurrentCultureIgnoreCase); Specialized Dictionary Classes ListDictionary Este dicionário de dados é uma implementação simples da Interface IDictionary. Ele é muito menor e mais rápido quando comparado com o Hashtable e se o número de elemento for menor ou igual a 10 e, não deve ser utilizado se a performance é importante para uma coleção com muitos elementos. Os itens que estão dentro deste dicionário não garantem que estarão em uma ordem específica e o código que você escreve não deve depender da ordem corrente de adição. A sua utilização em termos de escrita de código é idêntica ao Hashtable. É necessário instanciá-lo e, através do método Add, adicionar a chave e valor. HybridDictionary

Page 87: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 36

Israel Aece | http://www.projetando.net

36

Essa coleção tem um comportamente um tanto quanto especial. Trata-se também de um dicionário de dados que implementa a Interface IDictionary e que, internamente, armazena os dados em um objeto do tipo ListDictionary enquanto a coleção é pequena e, quando a coleção ganha um grande número de elemento, automaticamente o repositório é trocado por uma Hashtable. Com este comportamento, esta coleção é recomendada para casos onde o número de elementos é desconhecido, onde ele garantirá a performance enquanto a coleção estiver pequena e também para quando esta mesma coleção ganhar uma proporção maior. Além disso, essa coleção recebe em seu construtor um valor booleano indicando se a coleção vai ou não ignorar a diferença entre maiúsculas e minúsculas quando for comparar uma chave. Mais uma vez, a utilização da coleção HybridDictionary em termos de escrita de código é idêntica ao Hashtable. É necessário instanciá-lo e, através do método Add, adicionar a chave e valor. OrderedDictionary Essa classe implementa a Interface IOrderedDictionary que representa uma coleção indexada através de pares de chave e valor. Toda coleção que implementa essa Interface pode acessar seus elementos através de um chave ou índice. É basicamente uma forma de combinar um ArrayList com um Hashtable, pois os elementos do ArrayList somente são acessíveis através de índices e o Hashtable, podemos acessar os elementos através de uma chave. Como a Interface IOrderedDictionary também fornece uma propriedade default (indexer em Visual C#), temos agora duas propriedades deste tipo para recuperar um determinado elemento. A diferença entre as duas propriedade é que uma delas receberá um número inteiro que representará o elemento que desejamos recuperar; a segunda, receberá um objeto que representa a chave que foi utilizada para inserir o elemento dentro da coleção. Podemos visualizar isso através do código abaixo: VB.NET Imports System.Collections.Specialized Dim dic As New OrderedDictionary dic.Add("AC", "Acre") dic.Add("RJ", "Rio de Janeiro") dic.Add("SP", "São Paulo") Console.WriteLine(dic("RJ")) Console.WriteLine(dic(1))

Page 88: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 37

Israel Aece | http://www.projetando.net

37

C# using System.Collections.Specialized; OrderedDictionary dic = new OrderedDictionary(); dic.Add("AC", "Acre"); dic.Add("RJ", "Rio de Janeiro"); dic.Add("SP", "São Paulo"); Console.WriteLine(dic["RJ"]); Console.WriteLine(dic[1]); Ambas as formas (através da chave e através do índice) retornarão “Rio de Janeiro”. Specialized Named Collection Classes NameObjectCollectionBase Esta é uma classe abstrata para as coleções considerada “named collections”. Internamente ela mantém um objeto do tipo Hashtable onde sua chave é uma string e seu valor um object. Como trata-se de uma classe abstrata, você pode estar customizando a sua própria coleção a partir desta classe base. Quando for criar a sua própria coleção herdando desta, não se preocupe em precisar implementar nenhum método. Apenas alguns métodos são denotados como protected virtual (Protected Overridable em Visual Basic .NET), quais você pode customizar para a sua coleção. NameValueCollection Derivada de NameObjectCollectionBase, esta é uma coleção que pode ser acessada através de uma chave ou um índice. Mas o diferencial desta classe é mesmo a possibilidade de armazenar múltiplas valores (strings) através de uma única chave. Isso quer dizer que você pode adicionar chaves repetidas que, internamente, a coleção se encarrega de ajustar para quando desejar extrair os valores, todos eles sejam retornados. Alguns exemplos típicos deste tipo de coleção são as propriedades QueryString, Forms e Headers da classe HttpRequest, utilizada em aplicações ASP.NET. Para exemplificar a utilização desta coleção, analise o código abaixo: VB.NET Imports System.Collections.Specialized Dim queryStrings As New NameValueCollection queryStrings.Add("Alunos", "José") queryStrings.Add("Alunos", "Ivan")

Page 89: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 38

Israel Aece | http://www.projetando.net

38

queryStrings.Add("Alunos", "Mario") queryStrings.Add("Alunos", "Castro") queryStrings.Add("Alunos", "Mendes") queryStrings.Add("Instrutor", "Israel") For Each s As String In queryStrings.AllKeys Console.WriteLine("{0} - {1}", s, queryStrings(s)) Next C# using System.Collections.Specialized; NameValueCollection queryStrings = new NameValueCollection(); queryStrings.Add("Alunos", "José"); queryStrings.Add("Alunos", "Ivan"); queryStrings.Add("Alunos", "Mario"); queryStrings.Add("Alunos", "Castro"); queryStrings.Add("Alunos", "Mendes"); queryStrings.Add("Instrutor", "Israel"); foreach (string s in queryStrings.AllKeys) Console.WriteLine("{0} - {1}", s, queryStrings[s]); Como podemos visualizar, é perfeitamente permitido adicionar chaves iguais que a coleção trabalha sem nenhum problema. Em seguinda, dentro do laço percorremos a coleção distinta de chaves e, através de uma propriedade default (indexer em Visual C#) da classe NameValueCollection, passamos a chave corrente e ela retornará todos os valores que estão vinculados a chave informada. Para ambos os código, o retorno será o seguinte: Alunos - José,Ivan,Mario,Castro,Mendes Instrutor – Israel Bit Structures BitVector32 e BitVector32.Section Essas duas estruturas trabalham em conjunto e permitem você armazenar dentro dela valores inteiros ou valores booleanos utilizando apenas 32 bits de memória. Essa estrutura tem uma performance superior quando comparado a classe BitArray, justamente por manipular tipos-valor e não tipos-referência. Comparer

Page 90: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 39

Israel Aece | http://www.projetando.net

39

Ambos namespaces System.Collections e System.Collections.Generic possuem classes com implementações padrões das Interfaces IComparer e IComparer<T>, respectivamente. Os dois tópicos abaixo explicam e ilustram a utilização de cada uma dessas classes. Comparer Primário (System.Collections) Dentro deste namespace temos uma classe chamada Comparerm que possui uma implementação básica da Interface IComparer (discutida no capítulo 1). Essa classe recebe em seu construtor um objeto do tipo CultureInfo (contido no namespace System.Globalization) utilizando isso para a globalização da aplicação, pois em diferentes culturas, podemos ter diferentes formas de comparação e também de resultados. O exemplo abaixo ilustra como utilizá-la, especificando a nossa cultura (“pt-BR”) para efetuar a comparação: VB.NET Imports System.Collections Imports System.Globalization Dim comp As New Comparer(New CultureInfo("pt-BR")) Console.WriteLine("Comparando Paulo e paulo : {0}", _ comp.Compare("Paulo", "paulo")) C# using System.Collections; using System.Globalization; Comparer comp = new Comparer(new CultureInfo("pt-BR")); Console.WriteLine("Comparando Paulo e paulo : {0}", comp.Compare("Paulo", "paulo")); Como já sabemos, o método Compare retorna um número inteiro indicando se um objeto é menor, igual ou maior que o outro. No nosso caso, será retornado 1, indicando que “Paulo” é maior que “paulo”. Essa classe ainda possui um membro público estático chamado Default, que retorna uma instância da classe Comparer com a cultura especificada dentro da thread atual (Thread.CurrentCulture). Comparer Genérico (System.Collections.Generic) Quando necessitamos ordenar uma lista, como por exemplo, a classe List<T>, necessitamos que os objetos contidos nela sejam ordenados alfabéticamente. Sendo assim, quando você chama o método Sort da classe List<T> e o seu objeto não

Page 91: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 40

Israel Aece | http://www.projetando.net

40

implementar a Interface genérica IComparable<T> ou a Interface primária IComparable, uma excessão é disparada. Como trata-se de uma classe genérica chamada Comparer<T>. O fato dela ser genérica, é necessário especificarmos na sua chamada, o tipo com o qual ela irá trabalhar. Para o nosso exemplo, desejaremos ordenar uma lista, que contém objetos do tipo Usuario, através do nome (string), de forma alfabética. Essa classe fornece uma propriedade estática, de somente leitura chamada Default, que retorna um comparer específico para o tipo informado que, no nosso caso, será uma string. Para o exemplo, foi construído uma classe chamada Usuario que, em seu construtor, é passado o nome do mesmo e, expõe uma propriedade de escrita/leitura chamada Nome. O trecho de código abaixo exibe na íntegra a construção da classe e também o código responsável pela ordenação da lista de usuários: VB.NET Public Class Usuario Private _nome As String Public Sub New(ByVal nome As String) Me._nome = nome End Sub Public Property Nome() As String Get Return Me._nome End Get Set(ByVal value As String) Me._nome = value End Set End Property End Class ‘... Imports System.Collections.Generic Sub Main() Dim l As New List(Of Usuario) l.Add(New Usuario("Israel")) l.Add(New Usuario("Roberto")) l.Add(New Usuario("Anderson")) l.Add(New Usuario("Paulo")) l.Add(New Usuario("Leandro")) l.Sort(New Comparison(Of Usuario)(AddressOf InternalSort)) For Each u As Usuario In l Console.WriteLine(u.Nome) Next

Page 92: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 41

Israel Aece | http://www.projetando.net

41

End Sub Private Function InternalSort(ByVal u1 As Usuario, ByVal u2 As Usuario) As Integer Return Comparer(Of String).Default.Compare(u1.Nome, u2.Nome) End Function C# public class Usuario { private string _nome; public Usuario(string nome) { this._nome = nome; } public string Nome { get { return this._nome; } set { this._nome = value; } } } //... using System.Collections.Generic; static void Main(string[] args) { List<Usuario> l = new List<Usuario>(); l.Add(new Usuario("Israel")); l.Add(new Usuario("Roberto")); l.Add(new Usuario("Anderson")); l.Add(new Usuario("Paulo")); l.Add(new Usuario("Leandro")); l.Sort(new Comparison<Usuario>(InternalSort)); foreach (Usuario u in l) Console.WriteLine(u.Nome); } private static int InternalSort(Usuario u1, Usuario u2) {

Page 93: Livro de .NET - Israel Aece

Capítulo 2 – Trabalhando com Coleções 42

Israel Aece | http://www.projetando.net

42

return Comparer<string>.Default.Compare(u1.Nome, u2.Nome); } Nota: Lembrando que no Visual C# há a possibilidade de utlizarmos os métodos anônimos para evitar a criação de um procedimento adicional.

Page 94: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 1

Israel Aece | http://www.projetando.net

1

Capítulo 3 Utilização de Assemblies Introdução Até o momento, escrevemos o código dentro do Visual Studio .NET e, pressionando F5 ou Ctrl + F5 (Build) rodamos a aplicação para ela ser executada e, conseqüentemente, testamos para ver se o resultado é o esperado. Mas e nos bastidores, o que realmente acontece? A finalidade deste capítulo é justamente abordar o processo que é feito quando “rodamos” a aplicação. O que acontece por detrás disso é a compilação do código escrito em um Assembly. Além de entedermos o processo de geração do Assembly, vamos analisar a forma que temos para “assinar” o Assembly e, assim, garantirmos uma maior segurança e unicidade. Ainda nesta primeira parte, analisaremos os tipos de Assemblies e também as formas que temos para distribuí-los; como embutir arquivos de recursos dentro de um Assembly e como acessá-lo. Já na segunda parte do capítulo, será abordado as Installer Classes, quais fornecem uma possibilidade de automatizamos a instalação dos Assemblies nos clientes. Finalmente, veremos alguns funcionalidades disponíveis para criarmos seções de configurações customizadas dentro do arquivo de configuração da aplicação. Isso irá permitir termos uma aplicação mais flexível e bem mais intuitiva para os clientes que irão instalá-la. O que são Assemblies? No primeiro capítulo analisamos o CLR – Common Language Runtime que, como o próprio nome diz, é um runtime comum para todas as aplicações que utilizam o .NET Framework. Sendo assim, o CLR não conhece nada sobre a linguagem de programação que você escolheu para melhor expressar suas intenções. Quando você executa a aplicação dentro do Visual Studio .NET, o compilador da linguagem escolhida é encarregado verificação da sintaxe e da “escrita correta”, analisando o seu código fonte e certificando do que aquilo que escreveu faz algum sentido. Entre os vários compiladores disponíveis, temos os mais comuns: vbc.exe (Visual Basic .NET) e csc.exe (Visual C#). Para entendermos melhor o processo de compilação e criação do Assembly, vamos utilizar neste momento um utilitário de linha de comando para isso, deixando temporariamente, o Visual Studio .NET de lado. Sendo assim, você pode criar um arquivo de código (Visual Basic .NET ou Visual C#) no Notepad. Como não teremos o auxílio do Visual Studio .NET neste momento, deveremos utilizar o compilador correspondente da linguagem para efetuarmos a compilação. Independente de qual linguagem e qual compilador está utilizando, o resultado da compilação de um único arquivo será sempre o mesmo: um módulo gerenciado. Um módulo gerenciado é um executável portável (PE), que necessita obrigatoriamente do CLR para poder funcionar. A figura abaixo ilusta este processo:

Page 95: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 2

Israel Aece | http://www.projetando.net

2

Imagem 3.1 – Criação de um módulo gerenciado.

Dentro da plataforma Microsoft .NET, um Assembly é um código parcialmente compilado, tratando-se de um agrupamento lógico de um ou mais módulos gerenciados ou arquivos de recursos. Além disso, um Assembly é a menor unidade de reutilização, segurança e controle de versão. Dizemos parcialmente compilado, porque o código está em uma linguagem intermediária, chamada de MSIL: Microsoft Intermediate Language. Existem dois tipos de Assembly: EXE (Executable) e DLL (Dynamic Link Library). O primeiro deles é gerado quando uma aplicação do tipo Windows Forms, Windows Service ou Console são criadas; já o segundo, a DLL, trata-se de uma biblioteca de tipos que podem ser utilizados por várias outras aplicações. Dentro de cada um destes tipos de Assemblies, ainda tipos um sub-tipo que são: single-file assemblies e multi-file assemblies. O primeiro deles, single-file assemblies, são Assemblies que contém apenas um módulo gerenciado dentro dele, qual fará todo o trabalho necessário para a aplicação ou biblioteca funcionar. Já o segundo, multi-file assemblies, são compostos por mais de um módulo gerenciado, quais são colocados dentro deste Assembly. Vale lembrar que ainda podemos adicionar arquivos de recursos, como por exemplo: *.jpg, *.ico, *.txt, etc.. Veremos mais detalhadamente sobre arquivos de recurso ainda neste capítulo. Dentro dos Assemblies também temos o manifesto. O manifesto contém todas as informações sobre os itens que estão contidos dentro do Assembly, incluindo tudo o que ele expõe ao mundo. Ele também informa todas as dependências existentes no seu Assembly para com outros Assemblies. Todo Assembly requer um manifesto. Criação de Assemblies Para efeitos de exemplo, vamos criar dois tipos de Assemblies: single-file assemblies e multi-file assemblies. A começar pelo single-file, devemos criar um arquivo distinto para cada linguagem, um para Visual Basic .NET e outro para Visual C#. O exemplo abaixo

Page 96: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 3

Israel Aece | http://www.projetando.net

3

exibe a criação destes códigos utilizando Notepad. Cada um deles são salva em um diretório temporário chamado Temp dentro da unidade C com suas respectivas extensões. VB.NET Imports System Public Class BoasVindas Public Shared Sub Main() Console.WriteLine("Seja bem-vindo pelo VB.NET.") Console.ReadLine() End Sub End Class C# using System; public class BoasVindas { public static void Main() { Console.WriteLine("Seja bem-vindo pelo Visual C#."); Console.ReadLine(); } } Como pode reparar, trata-se de um arquivo simples. Para recapitular, cada um das linguagens tem um compilador próprio. No caso do Visual C#, o compilador é csc.exe e do Visual Basic .NET é o vbc.exe. Ambas estão contidos dentro do seguinte diretório: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727. Como esses utilitários são executados a partir de linha de comando e você optar por abrir o Visual Studio 2005 Command Prompt, não será necessário digitar o caminho todo até o executável para executá-lo. Abaixo está a chamada para o compilador, passando como parâmetro o arquivo a ser compilado e, em seguida, o output de cada compilador: VB.NET C:\Temp>vbc BoasVindasVB.vb Microsoft (R) Visual Basic Compiler version 8.0.50727.42 for Microsoft (R) .NET Framework version 2.0.50727.42 Copyright (c) Microsoft Corporation. All rights reserved. C# C:\Temp>csc BoasVindasCS.cs

Page 97: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 4

Israel Aece | http://www.projetando.net

4

Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.42 for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727 Copyright (C) Microsoft Corporation 2001-2005. All rights reserved. Com isso, finalizamos o processo de um single-file assembly. Criamos então dois executáveis: BoasVindasVB.exe e BoasVindasCS.exe. Agora, a idéia é termos mais de um arquivo (módulo gerenciado) e unir todos estes em um multi-file assembly. Para isso, devemos então criar dois arquivos de código. O primeiro trata-se de uma classe que será consumida por um outro código, em uma outra classe, em um arquivo distinto. O código abaixo mostra o arquivo Aluno.cs e Aluno.vb. Já os códigos que estão logo na seqüencia, utilizam as classes Aluno.cs e Aluno.vb: VB.NET Imports System Public Class Aluno Public Sub Show(ByVal nome As String) Console.WriteLine("Seja Bem-vindo: " & nome) End Sub End Class C# using System; public class Aluno { public void Show(string nome) { Console.WriteLine("Seja Bem-vindo: " + nome); } } VB.NET Imports System Public Class Escola Public Shared Sub Main() Dim aluno As New Aluno() aluno.Show("José Castro") End Sub

Page 98: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 5

Israel Aece | http://www.projetando.net

5

End Class C# using System; public class Escola { public static void Main() { Aluno aluno = new Aluno(); aluno.Show("José Castro"); } } Mais uma vez, utilizaremos os compiladores de cada linguagem para gerar o executável. Só que agora temos uma ligeira mudança em relação ao single-file assembly. Para cada um dos arquivos de código que temos, necessitamos criar um módulo gerenciado e, para isso, utilizamos a mesma técnica que o single-file assembly. Agora, quando referenciamos esta classe em um outro módulo gerenciado, é necessário primeiro adicionarmos o módulo pré-criado ao Assembly que será gerado. Os compiladores fornecem um parâmetro chamado addmodule que permitem adicionarmos a referência para o(s) módulo(s) que será(ão) utilizado(s). A sintaxe para isso é exibida através do output que o compilador gerou ao efetuar o processo: VB.NET C:\Temp>vbc /t:module Aluno.vb Microsoft (R) Visual Basic Compiler version 8.0.50727.42 for Microsoft (R) .NET Framework version 2.0.50727.42 Copyright (c) Microsoft Corporation. All rights reserved. C:\Temp>vbc /addmodule:Aluno.netmodule /out:Escola.exe Escola.vb Microsoft (R) Visual Basic Compiler version 8.0.50727.42 for Microsoft (R) .NET Framework version 2.0.50727.42 Copyright (c) Microsoft Corporation. All rights reserved. C# C:\Temp>csc /t:module Aluno.cs Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.42 for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727 Copyright (C) Microsoft Corporation 2001-2005. All rights reserved. C:\Temp>csc /addmodule:Aluno.netmodule /out:Escola.exe Escola.cs Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.42 for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727 Copyright (C) Microsoft Corporation 2001-2005. All rights reserved.

Page 99: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 6

Israel Aece | http://www.projetando.net

6

Claro que todo este processo está manual justamente por questões de entendimento. Quando você utiliza uma IDE, como por exemplo o Visual Studio .NET, tudo isso é feito de forma transparente, gerando apenas o EXE ou a DLL. O arquivo AssemblyInfo Este arquivo é criado por padrão nas maiorias das aplicações e é através dele que podemos colocar várias informações a nível de Assembly. Através de vários atributos fornecidos pelo .NET Framework, podemos definir várias informações como por exemplo: nome do produto, versão, empresa, descrição, cultura, segurança, etc.. Quando você cria uma aplicação utilizando o Visual Studio .NET, esse arquivo já é gerado automaticamente e você pode abrí-lo e configurar de acordo com a sua necessidade. O código abaixo mostra um arquivo AssemblyInfo padrão: VB.NET Imports System Imports System.Reflection Imports System.Runtime.InteropServices <Assembly: AssemblyTitle("VB")> <Assembly: AssemblyDescription("Exemplos em VB.NET.")> <Assembly: AssemblyCompany("People Computação")> <Assembly: AssemblyProduct("VB")> <Assembly: AssemblyCopyright("Copyright © 2007")> <Assembly: AssemblyTrademark("")> <Assembly: ComVisible(False)> <Assembly: Guid("4db1f6e9-653c-479f-ab0a-0c713b7d7822")> <Assembly: AssemblyVersion("1.0.0.0")> <Assembly: AssemblyFileVersion("1.0.0.0")> C# using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; [assembly: AssemblyTitle("CS")] [assembly: AssemblyDescription("Exemplos em C#.")] [assembly: AssemblyCompany("People Computação")] [assembly: AssemblyProduct("CS")] [assembly: AssemblyCopyright("Copyright © 2007")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] [assembly: Guid("653551f4-88ef-4c39-bcd9-0a00e39d4e42")]

Page 100: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 7

Israel Aece | http://www.projetando.net

7

[assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] Strong Names Imaginem que temos um Assembly chamado CalculosDiversos.dll criado pela empresa ABC Dev Company. Esse Assembly contém uma porção de tipos que são utilizados para os mais diversos cálculos. Esse Assembly será, muito provavelmente, por várias aplicações. Agora, temos um diretório conhecido que serve como repositório para os Assemblies que são utilizados pelas mais diversas aplicações e, um deles, é o CalculosDiversos.dll. Bem, neste momento, temos uma outra empresa que decide criar um mesmo Assembly, com o mesmo nome de arquivo e, se colocado dentro do diretório que é o nosso repositório de Assemblies, as aplicações que fazem o uso deste Assembly deixará de funcionar, pois a DLL foi sobrescrita e, como a última vence, estamos novamente com o inferno das DLLs em plena era .NET. Como é possível notar, diferenciar um Assembly apenas pelo nome de arquivo não é necessário para garantir a unicidade. Felizmente o .NET fornece uma forma de identificarmos o Assembly como sendo único e, para isso, utilizamos strong names. As informações que compõem um Assembly que é assinado por uma strong name consiste em quatro atributos, que o identificarão unicamente: o nome do arquivo (sem extensão), o número de versão, a cultura e um token de chave pública. Como o nome, cultura e a versão do Assembly podem repetir de uma empresa para outra, a Microsoft decidiu utilizar tecnologias de criptografias baseadas em chave pública/privada para garantir a unicidade do Assembly. Sendo assim, um Assembly assinado com uma strong name possui o nome do arquivo, a cultura, a versão e uma chave privada do publicador do mesmo. A Microsoft disponibilizou um utilitário de linha de comando que permite-nos gerar uma strong name para assinarmos os Assemblys que desejarmos. Esse utilitário chama-se sn.exe (sn = strong name) e, assim como os compiladores, encontra-se no seguinte local do disco: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727. Basicamente, para gerar um par de chave pública/privada, basta simplesmente fazer: C:\>sn -k C:\Temp\StrongName.snk O parâmetro –k indica ao utilitário para gerar a chave. Lembrando que, se quiser, poderá abrir o Visual Studio 2005 Command Prompt para poupar a digitação do caminho completo até o utilitário. O arquivo gerado através do comando acima conterá as chaves pública e privada.

Page 101: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 8

Israel Aece | http://www.projetando.net

8

Assinando o Assembly Agora que já sabe como criar uma strong name, você precisa referênciá-la no seu projeto. Para isso, você deverá utilizar o atributo chamado AssemblyKeyFileAttribute que é fornecido pelo .NET Framework e configurá-lo dentro do arquivo AssemblyInfo. O código abaixo exemplifica o uso deste atributo: VB.NET <Assembly: AssemblyKeyFile("C:\Temp\StrongName.snk")> C# [assembly: AssemblyKeyFile(@"C:\Temp\StrongName.snk")] Quando o compilador encontra esse atributo, ele assinará o Assembly com a chave privada e irá incorporar a chave pública. Isso, além de garantir a unicidade do Assembly, garantirá também a chacagem de integridade, ou seja, passando pelas checagens de segurança do .NET Framework, irá garantir que o conteúdo do Assembly não foi alterado desde a sua última compilação. Formas de distribuição de Assemblies Assemblies privados Quando referenciamos um determinado Assembly no projeto que estamos desenvolvendo, esse componente geralmente será copiado para a pasta da aplicação. Outra possibilidade é quando fazemos uma referência direta para o mesmo, e o manipulamos via Reflection. Esses tipos de Assemblies são considerados Assemblies privados, pois são utilizados por uma ou mais aplicações específicas, mas sempre tendo uma cópia local para mesmo. Esse tipo de Assemblies apesar de funcionar bem em alguns cenários, dificulta o processo de deployment quando se trata de um cenário mais complexo, onde a quantidade de aplicações/usuários que utilizam esses Assemblies é muito grande. A alternativa a este método é a depositar esse Assembly no Global Assembly Cache, tema da próxima seção. Global Assembly Cache – GAC Como vimos acima, uma das maiores dificuldades quando trabalhamos com Assemblies privados é a questão do deployment. Mesmo um Assembly assinado com uma strong name permitiria, por exemplo, a execução lado-a-lado. É neste momento que entra em ação o GAC – Global Assembly Cache. Se um Assembly poderá ser carregado por múltiplas aplicações, esse Assembly deverá ser colocado em um local de conhecimento de todos. Esse local conhecido trata-se do GAC,

Page 102: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 9

Israel Aece | http://www.projetando.net

9

que é um centralizador dos Assemblies disponíveis para consumo. Quando um Assembly é adicionado ali, ele terá uma séria de benefícios, tais como:

• Facilidade de deploy, já que está tudo centralizado • Melhoria na performance • Possibilita a execução lado-a-lado de um mesmo Assembly com versões

diferentes, já que um diretório físico não resolve o problema, pois podemos ter dois componentes com o mesmo nome de arquivo.

Há três formas de inserirmos um determinado Assembly no GAC:

• Windows Explorer: Se navegar via Windows Explorer até o diretório C:\WINDOWS\ASSEMBLY você verá todos os Assemblies que estão no GAC daquela maquina específica. Você pode, via drag-and-drop, adicionar Assemblies ali, mas isso somente é interessante em ambiente de desenvolvimento.

• Utilitário Gacutil.exe: Este utilitário, fornecido também pelo SDK do .NET Framework, permite via linha de comando, interagir com o GAC, adicionando, removendo, listando, etc., Assemblies. Este utilitário é utlizando em ambientes de testes e desenvolvimento, nunca em produção.

• Installers: Permite adicionarmos a instalação dos componentes dentro do GAC via Windows Installer. Isso é perfeitamente útil quando desejamos empacotar o sistema em um projeto de setup para que o usuário final, muitas vezes sem muito conhecimento, possa instalar o sistema sem maiores dificuldades. Este é a forma que se utiliza para a instalação de um componente em uma máquina cliente, já que o .NET Framework redistribuível não possui o utilitário GacUtil.exe.

A imagem abaixo mostra o Global Assembly Cache – GAC, já com uma porção de Assemblies adicionadas pela instalação do .NET Framework. Cada linha contém o nome do Assembly, a versão, a cultura e a chave pública:

Page 103: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 10

Israel Aece | http://www.projetando.net

10

Imagem 3.2 – Global Assembly Cache – GAC.

Instalando um Assembly no GAC Para instalar um Assembly no GAC, somente é permitido se o usuário que estiver fazendo isso tiver privilégios administrativos e, o mais importante, o Assembly deverá ter uma strong name definida. O trecho de código abaixo exibe o resultado da adição de um componente dentro do GAC e, a imagem a seguir, exibe o GAC já com o componente devidamente adicionado: C:\>gacutil -i C:\Temp\ComponenteComum.dll Microsoft (R) .NET Global Assembly Cache Utility. Version 2.0.50727.42 Copyright (c) Microsoft Corporation. All rights reserved. Assembly successfully added to the cache

Imagem 3.3 – Componete já adicionado ao GAC.

Nota: Somente adicione no GAC os Assemblies que realmente serão compartilhados entre várias aplicações, caso contrário, o Assembly deverá ser implantando privadamente, ou seja, junto ao diretório da aplicação. Embutindo arquivos dentro de um Assembly Como vimos acima, um contém módulos gerenciados, metadados, manifesto, código IL e os recursos. Recursos ou arquivos de recursos são informações que também são embutidas dentro de um determinado Assembly que são informações necessárias para o funcionamento do Assembly. Geralmente essas informações são imagens, arquivos xml, arquivos texto, mensagens, etc.. Se analisarmos o próprio .NET Framework, ele possui uma porção de recursos e, como exemplo, podemos citar o Assembly System.Windows.Forms.dll que embuti várias imagens que são utilizadas pelos controles que lá estão contidos quando são colocados na barras de ferramentas do Visual Studio .NET. Um outro exemplo é o caso do Assembly System.Web.dll que, na seção de recursos, além de imagens, possui também arquivos javascript que são utilizados pelos controles do ASP.NET.

Page 104: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 11

Israel Aece | http://www.projetando.net

11

Quando estamos em um mundo mais próximo da nossa realidade, ícones ou imagens que nossa aplicação depende para ser exibido nos formulários da aplicação, poderiam ser também embutidos dentro do Assembly. Sendo assim, podemos então adicionar uma imagem qualquer dentro do projeto e, clicando com o botão direito do mouse e indo até a opção “Propriedades”, a janela de Propriedades é exibida. Encontre a propriedade Build Action e a defina como Embedded Resource. Isso fará com que o arquivo (seja texto, imagem, ícone, etc.) seja embutido dentro do Assembly. Supondo que a imagem adicionada foi Background.jpg, a chave para acessá-la será: NomeProjeto.Background.jpg. O trecho de código abaixo exibe a forma de recuperar a imagem via código e definí-la como sendo o Background de um formulário Windows Forms. VB.NET Imports System.IO Imports System.Reflection Dim asb As [Assembly] = Assembly.GetExecutingAssembly() Me.BackgroundImage = _ New Bitmap(asb.GetManifestResourceStream("VB.Background.jpg")) C# using System.IO; using System.Reflection; Assembly asb = Assembly.GetExecutingAssembly(); this.BackgroundImage = new Bitmap(asb.GetManifestResourceStream("CS.Background.jpg")); A possibilidade de incluir arquivos dentro de um Assembly facilita a distribuição, já que você não precisa se preocupar em manter os caminhos dos arquivos, já que eles estão embutidos e a forma de acessá-los é diferente. Além disso, temos uma maior segurança, pois depois de compilado, não é mais possível alterá-lo a menos que, abra o projeto e edite o arquivo e, finalmente, gere um novo Assembly contendo as novas informações. Instalação de Assemblies Instaladores padrões Desde as primeiras versões do .NET Framework temos uma nova categoria de tipos de projetos que estão embutidos dentro das templates do Visual Studio .NET. Essa categoria trata-se dos projetos de Setup.

Page 105: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 12

Israel Aece | http://www.projetando.net

12

Apesar de ser possível utilizarmos a técnica de XCOPY para a instalação de projetos no cliente. O XCOPY trata-se de simplesmente copiar o(s) arquivo(s) do projeto em um dispositivo qualquer, como por exemplo, CD, Pen-drive, etc., e levar até o cliente e lá copiar todos os arquivos para o disco da máquina onde o sistema irá rodar. A desinstalação é tão simples quanto a instalação, bastante apenas localizar a pasta onde o projeto está instalado e excluí-la do disco. Apesar de simples, isso nem sempre é o ideal por muitas razões:

1. Não existe segurança. 2. Exige que a pessoa que irá instalar, tenha um conhecimento razoável. 3. Não conseguimos automatizar as configurações na máquina durante a

instalação. Visando todos esses problemas, a Microsoft decidiu criar os projetos de Setup para auxiliar essa tarefa bastante comum. Os projetos são bastante interessantes e permitem uma construção muito rápido, sem a necessidade de adquirir componentes de terceiros. Um dos grandes benefícios é a possibilidade de gerar uma instalação “transacionada”, ou seja, se durante a instalação algo falhar, seguramente ele irá desfazer todas as mudanças que o instalador fez. Atualmente temos os seguintes projetos disponíveis: Tipo de Projeto Descrição Setup Project Utilizado para a criação de projetos de instalação para

aplicações que rodam em ambiente Windows (Windows Forms).

Web Setup Project Utilizado para criação de projetos de instalação para aplicações ASP.NET.

Merge Module Project Utilizado para criar um instalador para componentes compartilhados.

CAB Project Utilizado para gerar e comprimir arquivos CAB para disponibilizá-los para download.

Customizando da instalação e desinstalação Os projetos de Setup atendem perfeitamente para a instalação básica de um software: cópia de arquivos, criação de pastas, criação de items no menu Iniciar do Windows e atalho na área de trabalho do usuário. Mas há cenários onde você quer customizar a instalação de um projeto. Geralmente esse projeto requer diversas configurações que você precisará informar ou que ele mesmo possa criar durante o processo de instalação. Essas configurações são, por exemplo, criação de base de dados e seus objetos dentro do SQL Server, criação de arquivos no disco, criação de Message Queue, entre outras várias possibilidades. Para resolver esse problema, podemos utilizar o que chamamos de classes de instalação ou Installer Classes.

Page 106: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 13

Israel Aece | http://www.projetando.net

13

Fornecidas pelo namespace System.Configuration.Install (para ter acesso as classes, é necessário fazer a referência à System.Configuration.Install.dll), essas classes disponibilizam uma gama de funcionalidades que podemos customizar a instalação do nosso projeto. Entre as classes disponíveis, temos a classe Installer, AssemblyInstaller, ComponentInstaller, InstallContext e a classe TransactedInstaller. Como a idéia aqui é customizar toda a instalação de um determinado software, essas classes vão nos auxiliar durante toda a criação desta forma customizada de instalar uma aplicação. Installer A classe Installer é responsável por fornecer todas as funcionalidades necessárias para a customização da instalação, sendo a classe base para todos os outros Installers dentro do .NET Framework. O passos a seguir definem o processo que deve ser realizado para que o instalador seja construído e executado corretamente:

1. Criar um instalador, herdando da classe Installer. 2. Sobrescreva os métodos Install, Commit, Rollback e Uninstall. Esses métodos

não em visibilidade pública, pois são marcados como protected. Serão invocados automaticamente a partir da instalação do software.

3. Adicione o atributo RunInstallerAttribute a classe instaladora que está criado. 4. Invoque o instalador. Pode ser através do utilitário de linha de comando

installutil.exe ou através da classe AssemblyInstaller, qual veremos mais detalhadamente logo abaixo.

Esta classe possui uma propriedade denominada Installers, que recebe como parâmetro uma coleção de Installers que, por sua vez, cada elemento é um tipo Installer. Esses Installers farão parte de uma mesma instalação e nos permite ter uma hierarquia de instaladores. Cada um dos métodos Install, Commit, Rollback e Uninstall recebem como parâmetro uma coleção do tipo chave-valor (IDictionary) para que você possa receber informações entre os métodos da instalação corrente e, quando o mesmo é finalizado, eles valores são persistidos para mais tarde, quando a desinstalação acontecer, você consiga desfazer o que tinha feito. Para exemplificar, imagine que durante a instalação, eu preciso criar um arquivo no disco com um nome que é gerado dinamicamente. Esse nome é gerado e então, necessitamos guardá-lo para que no momento da desinstalação da aplicação (ou no Rollback da instalação), você consiga excluir o arquivo gerada pelo instalador. O atributo RunInstallerAttribute especifica que quando o instalador customizado for executado para instalar um determinado Assembly, ele deverá invocar todas as classes que estão decoradas com este atributo e que em seu construtor, o valor definido esteja como True. Para transformarmos isso em código, abaixo é mostrado um exemplo onde temos uma aplicação console que é o sistema que desenvolvemos e que o cliente precisará utilizar e,

Page 107: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 14

Israel Aece | http://www.projetando.net

14

em seguida, a classe instaladora do nosso sistema. Ainda na aplicação que o usuário terá acesso, além de termos os códigos normais, teremos que ter também o instalador que é a classe que herda da classe Installer e aplica o atributo RunInstallerAttribute. VB.NET Console.WriteLine("Esta é aplicação que o cliente solicitou...") Console.ReadLine() C# Console.WriteLine("Esta é aplicação que o cliente solicitou..."); Console.ReadLine();

Instalador (dentro da mesma aplicação) VB.NET Imports System Imports System.IO Imports System.Collections Imports System.ComponentModel Imports System.Configuration.Install Namespace Aplicacao <RunInstaller(true)> _ Public Class Instalador Inherits Installer Public Overrides Sub Install(ByVal stateSaver As IDictionary) Dim file As String = "c:\ViveraDuranteAplicacao.txt" Using sw As New StreamWriter(file) sw.Write("Conteudo!") End Using stateSaver("Arquivo") = file MyBase.Install(stateSaver) End Sub Public Overrides Sub Uninstall(ByVal savedState As IDictionary) Dim file As String = savedState("Arquivo").ToString() If File.Exists(file) Then File.Delete(file) End If MyBase.Uninstall(savedState) End Sub End Class End Namespace C#

Page 108: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 15

Israel Aece | http://www.projetando.net

15

using System; using System.IO; using System.Collections; using System.ComponentModel; using System.Configuration.Install; namespace Aplicacao { [RunInstaller(true)] public class Instalador : Installer { public override void Install(IDictionary stateSaver) { string file = @"c:\ViveraDuranteAplicacao.txt"; using (StreamWriter sw = new StreamWriter(file)) { sw.Write("Conteudo!"); } stateSaver["Arquivo"] = file; base.Install(stateSaver); } public override void Uninstall(IDictionary savedState) { string file = savedState["Arquivo"].ToString(); if (File.Exists(file)) File.Delete(file); base.Uninstall(savedState); } } } Como podemos reparar, somente optamos por sobrescrever os métodos Install e Uninstall. Como já era de se esperar, o primeiro deles ocorre apenas quando a aplicação for instalada. Como estamos colocando o nome do arquivo gerado dentro da coleção que vem como parâmetro (stateSaver), ele valor será mantido e mais tarde, quando o método Uninstall acontecer, ele conseguirá recuperar o nome do arquivo para que ele possa excluí-lo. A classe Installer ainda contém uma propriedade bastante útil chamada Context do tipo InstallContext que, como o próprio nome diz, traz informações a respeito do contexto de instalação do Assembly, como por exemplo, o local do arquivo de log da instalação, o local do arquivo para salvar informações requisitadas pelo método Uninstall e, os argumentos que são passados através do utilitário installutil.exe, quais são mapeados

Page 109: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 16

Israel Aece | http://www.projetando.net

16

para entradas dentro da propriedade Parameters, do tipo StringDictionary. Além disso, a classe InstallContext ainda fornece um método chamado LogMessage que permite escrevermos algo na console e no arquivo de log da instalação. Eventos A classe Installer também fornece uma porção de eventos interessantes para comunicar-se com o cliente e notificá-lo de acordo com o progresso da instalação. A tabela abaixo descreve cada um destes eventos disponíveis: Evento Descrição AfterInstall Ocorre depois que todos os métodos Install de todos os instaladores

contidos na propriedade Installers foram executados. AfterRollback Ocorre depois que todos os métodos Rollback de todos os

instaladores contidos na propriedade Installers foram executados. AfterUninstall Ocorre depois que todos os métodos Uninstall de todos os

instaladores contidos na propriedade Installers foram executados. BeforeInstall Ocorre antes que o método Install de cada instalador contido na

propriedade Installers seja executado. BeforeRollback Ocorre antes que o método Rollback de cada instalador contido na

propriedade Installers seja executado. BeforeUninstall Ocorre antes que o método Uninstall de cada instalador contido na

propriedade Installers seja executado. Committed Ocorre depois de que todos os instaladores contidos na propriedade

Installers concretizaram o trabalho com êxito. Committing Ocorre antes de todos os instaladores contidos na propriedade

Installers concretizaram o trabalho. AssemblyInstaller Para fins de exemplo, também criaremos uma aplicação console para que sirva como instalador do sistema acima criado. Para conseguirmos instalar a aplicação dinamicamente, ou seja, via código, é será necessário utilizarmos uma nova classe, também exposta pelo namespace System.Configuration.Install. Neste momento entra em cena a classe AssemblyInstaller. Também derivada da classe Installer, esta classe é responsável por instalar um Assembly. Dado o caminho físico completo até o Assembly, essa classe carrega-o e extrair de dentro dele todos os instaladores lá contidos e os executam. Para que isso seja possível, devemos seguir rigorosamente os passos que vimos acima para a criação do Installer e mantendo todos os instaladores com o modificador de acesso definido como public, caso contrário, não é possível ser acessado externamente. O trecho de código abaixo cria a instância da classe AssemblyInstaller apontando para o Assembly que deseja instalar.

Page 110: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 17

Israel Aece | http://www.projetando.net

17

Quando invocamos os métodos Install, Commit, Rollback e Uninstall da classe AssemblyInstaller, internamente ela invoca os respetivos métodos dos instaladores do Assembly informado. O trecho de código invoca o método Install e Commit: VB.NET Imports System Imports System.Collections Imports System.Configuration.Install Dim savedState As IDictionary = New Hashtable() Dim cmdLine() As String = New String() {"/LogFile=Log.txt"} Using installer As New AssemblyInstaller ("c:\Temp\Aplicacao.exe", cmdLine) installer.Install(savedState) installer.Commit(savedState) End Using Console.WriteLine("Aplicao Instalada.") Console.ReadLine() C# using System; using System.Collections; using System.Configuration.Install; IDictionary savedState = new Hashtable(); string[] cmdLine = new string[] { "/LogFile=Log.txt" }; using (AssemblyInstaller installer = new AssemblyInstaller("c:\\Temp\\Aplicacao.exe", cmdLine)) { installer.Install(savedState); installer.Commit(savedState); } Console.WriteLine("Aplicação Instalada."); Console.ReadLine(); Mas e se desejarmos desinstalar a aplicação? Bem, é tão simples quanto a instalação. Cria-se o objeto AssemblyInstaller apontando para o Assembly, passando os argumentos se desejar e agora basta chamar o método Uninstall ou invés de Install e Commit. ComponentInstaller Mais um derivado da classe Installer, este componente permite a possibilidade de interargimos com o instalador, passando para ele uma instância de um objeto para que ele possa utilizar para a instalação correta da aplicação.

Page 111: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 18

Israel Aece | http://www.projetando.net

18

Assim como a classe Installer, você também deve criar um instalador, mas agora, utilizando como base o ComponentInstaller. Esta classe basicamente fornece um método abstrato chamado de CopyFromComponent que recebe em seu parâmetro uma instância de um objeto que implemente a Interface IComponent. Um exemplo típico é quando você precisa criar objetos dentro do SQL Server (Stored Procedures, Tabelas, etc.). É notável que para isso precisamos de uma conexão com o servidor de banco de dados. Como pode haver a possibilidade desta conexão já estar disponível, poderia simplesmente mandar essa conexão para que o instalador possa utilizar para a criação dos objetos que mencionamos acima. TransactedInstaller Finalmente temos o instalador TransactedInstaller, qual também é derivado da classe Installer. Esse instalador tem um funcionalida extremamente útil, envolve a instalação da aplicação em um ambiente transacionado. Isso quer dizer que, se alguma exceção ocorrer durante o processo de instalação, o TransactedInstaller deixará o computador em qual está sendo instalado em um estado consistente. Basicamente, ele garantirá que o método Rollback será executado e, sendo assim, não esqueça de sobrescrever o método em seu instalador para poder contemplar uma possível exceção que possa vir a acontecer. O código abaixo é uma versão transacionada do qual vimos um pouco mais acima, quando ainda falávamos da classe AssemblyInstaller: VB.NET Imports System Imports System.Collections Imports System.Configuration.Install Dim savedState As IDictionary = New Hashtable() Dim cmdLine() As String = New String() {"/LogFile=Log.txt"} Using installer As New AssemblyInstaller ("c:\Temp\Aplicacao.exe", cmdLine) Using tran As New TransactedInstaller() tran.Context = installer.Context tran.Installers.Add(installer) tran.Install(savedState) tran.Commit(savedState) End Using End Using Console.WriteLine("Aplicao Instalada.") Console.ReadLine C# using System;

Page 112: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 19

Israel Aece | http://www.projetando.net

19

using System.Collections; using System.Configuration.Install; IDictionary savedState = new Hashtable(); string[] cmdLine = new string[] { "/LogFile=Log.txt" }; using (AssemblyInstaller installer = new AssemblyInstaller(@"c:\\Temp\\Aplicacao.exe", cmdLine)) { using (TransactedInstaller tran = new TransactedInstaller()) { tran.Context = installer.Context; tran.Installers.Add(installer); tran.Install(savedState); tran.Commit(savedState); } } Console.WriteLine("Aplicação Instalada."); Console.ReadLine(); Arquivos de configuração O que são arquivos de configuração Os arquivos de configuração são arquivos que colocamos no interior da aplicação que, permitem de uma forma bem flexível, customizar parâmetros para que a mesma possa trabalhar corretamente. A flexibilidade é verdadeira porque alguns parâmetros variam de um ambiente para outro, como é o caso, por exemplo, de uma conexão com o banco de dados. Quando a aplicação está sendo desenvolvido, muito provavelmente, a conexão tem que apontar para o servidor de banco de dados de desenvolvimento. Agora, quando mandamos essa aplicação para o cliente, as configurações de servidor de base de dados são completamente diferentes. Como não é possível compilar a aplicação no cliente, seria interessante se, de alguma forma, conseguíssemos alterar essa configuração sem a necessidade de recompilar a aplicação. Temos dois tipos de arquivo de configuração: App.Config e Web.Config. O primeiro deles é utilizado em aplicações executáveis, como por exemplo, aplicações Windows, Windows Services, etc; já o segundo, é utilizado por aplicações Web/ASP.NET, também com a finalidade de configuração de parâmetros que a aplicação necessita para trabalhar. Além as configurações de conexões com a base de dados, ainda temos uma seção interessante, que é a AppSettings. Essa seção permite colocarmos qualquer tipo de

Page 113: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 20

Israel Aece | http://www.projetando.net

20

informação que necessitamos parametrizar na aplicação. Imagine que a sua aplicação manipule alguma fila de mensagens de Message Queue. Cada cliente tem a fila disponível para a sua aplicação utilizar mas, cada uma tem um nome diferente. Então para isso, podemos parametrizar o nome da fila no arquivo de configuração e, em cada cliente, você altera o nome, sem a necessidade de qualquer trabalho adicional. Para exemplificar a parametrização da conexão com o banco de dados e as informações de parâmetros via arquivo de configuração, o código dá uma idéia de como proceder: <?xml version="1.0"?> <configuration> <appSettings> <add key="MessageQueueName" value="FileDeMensagens" /> </appSettings> <connectionStrings> <add name="SqlConnectionString" connectionString="Data Source=.;Initial Catalog=MeuBanco;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration> Como podemos visualizar, através do elemento add podemos adicionar quantos parâmetros e conexões forem necessária para a aplicação poder trabalhar. O que precisa se atentar é com relação aos elementos key (em AppSetings) e em name (em connectionStrings), pois elas não podem repetirem dentro do mesmo arquivo. Agora, dentro da aplicação você pode acessar as informações que criamos no arquivo de configuração acima. Para isso, basta utilizar a classe ConfigurationManager que está contida dentro do namespace System.Configuration. Entre várias funcionalidades, essa classe expõe duas propriedades chamadas AppSettings e ConnectionStrings que retornam coleções de cada uma das seções. O trecho de código exibe a forma que utilizamos para poder recuperá-las: VB.NET Imports System.Configuration ‘... Console.WriteLine(ConfigurationManager.AppSettings("MessageQueueName")) Console.WriteLine(ConfigurationManager.ConnectionStrings("SqlConnectionString")) C# using System.Configuration;

Page 114: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 21

Israel Aece | http://www.projetando.net

21

//... Console.WriteLine(ConfigurationManager.AppSettings["MessageQueueName"]); Console.WriteLine(ConfigurationManager.ConnectionStrings["SqlConnectionString"]); Criando uma seção de configuração customizada O que vimos até o momento satisfaz uma boa parte das necessidades que temos. Mas agora, gostaríamos de criar uma seção de configuração própria na aplicação que estamos desenvolvendo para ter um maior controle e uma tipagem mas eficiente. A idéia aqui é mostrar como criar uma seção de configuração específica para a aplicação que estamos desenvolvendo. Para iniciar, mesmo tendo um namespace disponível na aplicação quando a criamos, é necessário adicionar a referência a uma DLL chamada System.Configuration.dll. Essa DLL disponibilizará para a aplicação várias classes que iremos utilizar para a construção dessa seção de configuração customizada. ConfigurationElement ConfigurationElement é uma classe abstrata que representa um determinado elemento dentro do arquivo de configuração. Como trata-se de uma classe abstrata, obrigatoriamente deve ser herdada em uma classe concreta que representará elementos de configuração XML, que tratam-se também dos parâmetros que precisamos para que nossa aplicação trabalhe/manipule. A classe concreta que herda de ConfigurationElement basicamente conterá as propriedades que desejam disponibilizar no arquivo de configuration para ser definido e, conseqüentemente, utilizado pela aplicação. Essas propriedades são configuradas com alguns atributos que irão definir toda as características individuais de cada propriedade, como por exemplo, o tipo, nome, valor padrão, etc. Para exemplificar a criação da seção customizada, vamos inicialmente analisar a implementação de ConfigurationElement logo abaixo: VB.NET Imports System.Configuration Public Class UrlConfigElement Inherits ConfigurationElement <ConfigurationProperty("name", DefaultValue:="Microsoft", IsRequired:=True, IsKey:=True)> _ Public ReadOnly Property Name() As String

Page 115: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 22

Israel Aece | http://www.projetando.net

22

Get Return MyBase.Item("name").ToString() End Get End Property <ConfigurationProperty("url", DefaultValue:="http://www.microsoft.com", IsRequired:=True)> _ <RegexStringValidator("\w+:\/\/[\w.]+\S*")> _ Public ReadOnly Property Url() As String Get Return MyBase.Item("url").ToString() End Get End Property <ConfigurationProperty("port", DefaultValue:=0, IsRequired:=False)> _ <IntegerValidator(MinValue:=0, MaxValue:=8080, ExcludeRange:=False)> _ Public ReadOnly Property Port() As Integer Get Return MyBase.Item("port").ToString() End Get End Property End Class C# using System.Configuration; public class UrlConfigElement : ConfigurationElement { [ConfigurationProperty("name", DefaultValue = "Microsoft", IsRequired = true, IsKey = true)] public string Name { get { return (string)this["name"]; } } [ConfigurationProperty("url", DefaultValue = "http://www.microsoft.com", IsRequired = true)] [RegexStringValidator(@"\w+:\/\/[\w.]+\S*")] public string Url { get { return (string)this["url"]; }

Page 116: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 23

Israel Aece | http://www.projetando.net

23

} [ConfigurationProperty("port", DefaultValue = (int)0, IsRequired = false)] [IntegerValidator(MinValue = 0, MaxValue = 8080, ExcludeRange = false)] public int Port { get { return (int)this["port"]; } } } Temos no código acima uma classe chamada UrlConfigElement que contém três propriedades: Name, Url e Port. Essa classe representa informações a respeito de um website específica com o seu nome e a porta. Cada uma dessas propriedades contém uma atributo chamado ConfigurationProperty que, como vimos acima, indica como ela será representada pelo XML dentro do arquivo de configuração. Esse atributo permite-nos informações algunas informações importantes a respeito da propriedades, uma forma de vincular as propriedades do código à configurações do arquivo de configuração. A tabela abaixo descreve as principais configurações que podemos fazer para cada propriedade individualmente: Propriedade Descrição DefaultValue Define um valor padrão para a propriedade que é utilizada quando não

é informada pelo arquivo de configuração. Geralmente é utilizada quando a propriedade IsRequired é definida como False.

Description Utilizado apenas para definir uma descrição. Ela não é utilizada pela aplicação.

IsKey Indica se a propriedade é a chave para o objeto, qual também será utilizada dentro de uma possível coleção dentro do arquivo de configuração.

IsRequired Indica se a propriedade é ou não obrigatória. Name Especifica o nome da propriedade que será utilizado no arquivo de

configuração. Type Define o tipo que a propriedade deverá ter no arquivo de configuração. Validators Os validadores disponibilizam uma forma interessante para validarmos as informações que são colocadas no arquivo de configuração. Assim como o atributo ConfigurationProperty, os validadores também são definidos via atributos. Todos os

Page 117: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 24

Israel Aece | http://www.projetando.net

24

validadores para esta finalidade herdam diretamente de uma classe abstrata chamada ConfigurationValidatorBase que define dois métodos que devem ser implementados: CanValidate e Validate. O primeiro deles, CanValidate define se um objeto pode ser validado baseando-se em um determinado tipo; o segundo e último método, Validate, determina a validação efetiva do objeto, indicando se ele está ou não de acordo com o que a aplicação espera para trabalhar. Existem vários validadores disponíveis dentro do .NET Framework, mais precisamente, dentro do namespace System.Configuration e, como já era de se esperar, podemos implementar o nosso próprio validador herdando de ConfigurationValidatorBase. Se analisarmos o trecho de código acima, veremos o uso de dois validadores diferentes: RegexStringValidator e IntegerValidator. O primeiro deles permite validar uma determinada string de acordo com uma regular expressions; já o IntegerValidator faz a verificação para saber se o valor informado é ou não do tipo Int32. Ambos atributos fornecem propriedades diferentes, que são necessárias de acordo com o tipo de validação. ConfigurationElementCollection Como o próprio nome diz, ConfigurationElementCollection, trata-se de uma coleção de elementos dentro do arquivo de configuração. A idéia por trás desta objeto/coleção está em permitir a declaração no arquivo de configuração de múltiplos elementos de um mesmo tipo que, no nosso caso, é o UrlConfigElement. A sua implementação não é muito complexa. Trata-se também de uma classe abstrata que fornece dois principais métodos que devem ser implementados na classe derivada: CreateNewElement e GetElementKey. O método CreateNewElement permite criarmos elementos de um determinado tipo que deve sempre retornar uma instância do objeto que estamos querendo armazenar dentro da nossa coleção. Já o método GetElementKey deve retornar a chave para o objeto, que é sempre a propriedade que está marcada com o atributo IsKey do atributo ConfigurationProperty dentro do objeto que herda de ConfigurationElement. O código abaixo demonstra como devemos proceder para utilizar a classe abstrata ConfigurationElementCollection: VB.NET Imports System.Configuration ‘... Public Class UrlColl Inherits ConfigurationElementCollection Protected Overloads Overrides Function CreateNewElement() As ConfigurationElement Return New UrlConfigElement() End Function Protected Overrides Function GetElementKey(ByVal element As

Page 118: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 25

Israel Aece | http://www.projetando.net

25

ConfigurationElement) As Object Return DirectCast(element, UrlConfigElement).Name End Function End Class C# using System.Configuration; //... public class UrlsCollection : ConfigurationElementCollection { protected override ConfigurationElement CreateNewElement() { return new UrlConfigElement(); } protected override Object GetElementKey(ConfigurationElement element) { return ((UrlConfigElement)element).Name; } } ConfigurationSection Finalmente, temos a classe (também abstrata) ConfigurationSection. Ela representa uma seção dentro do arquivo de configuração, que permite-nos customizar uma seção para um determinado tipo que criamos dentro da aplicação. Você deverá herdá-la para fornecer ao sistema uma forma programática de fortemente tipada de acessar as configurações que foram definidas no arquivo de configuração. Assim como aconteceu com a classe que herda de ConfigurationElement, a seção que vamos customizar a partir de ConfigurationSection precisa apenas das propriedades que a mesma irá expor para serem configuradas a partir do arquivo de configuração. Como a idéia aqui é também mostrar o uso da coleção que criamos um pouco mais acima então, uma das propriedades, irá retornar uma coleção com todos os elementos já configurados. Através do código abaixo podemos analisar a implementação da seção customizada, já com a propriedade que expõe a coleção de elementos do tipo UrlConfigElement e também um único elemento que irá expor apenas um objeto, também do tipo UrlConfigElement: VB.NET Imports System.Configuration ‘... Public Class UrlsSection

Page 119: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 26

Israel Aece | http://www.projetando.net

26

Inherits ConfigurationSection <ConfigurationProperty("name", DefaultValue:="Favoritos", IsRequired:=True, IsKey:=False)> _ <StringValidator(InvalidCharacters:=" ~!@#$%^&*()[]{}/;'\""|\\", MinLength:=1, MaxLength:=60)> _ Public ReadOnly Property Name() As String Get Return MyBase.Item("name").ToString() End Get End Property <ConfigurationProperty("simple")> _ Public ReadOnly Property Simple() As UrlConfigElement Get Return DirectCast(MyBase.Item("simple"), UrlConfigElement) End Get End Property <ConfigurationProperty("urls", IsDefaultCollection:=False)> _ Public ReadOnly Property Urls() As UrlColl Get Return DirectCast(MyBase.Item("urls"), UrlColl) End Get End Property End Class C# using System.Configuration; //... public class UrlsSection : ConfigurationSection { [ConfigurationProperty("name", DefaultValue = "Favoritos", IsRequired = true, IsKey = false)] [StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;'\"|\\", MinLength = 1, MaxLength = 60)] public string Name { get { return (string)this["name"]; } } [ConfigurationProperty("simple")] public UrlConfigElement Simple { get

Page 120: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 27

Israel Aece | http://www.projetando.net

27

{ return (UrlConfigElement)base["simple"]; } } [ConfigurationProperty("urls", IsDefaultCollection = false)] public UrlsCollection Urls { get { return (UrlsCollection)base["urls"]; } } } Temos então a propriedade Name que expõe uma string, a propriedade Simple que retorna uma instância (individual e isolada) do elemento UrlConfigElement e a propriedade UrlsCollections que, como o próprio nome diz, retorna uma coleção de objetos do tipo UrlConfigElement, conforme criamos um pouco mais acima. Registrando a seção customizada Depois de todas as classes criadas, chega o momento que precisamos configurar o arquivo de configuração para definir como estruturar o arquivo e também colocar o valor correspondente a cada uma das propriedades. Para registrarmos essa seção dentro do arquivo de configuração da aplicação é necessário utilizarmos o elemento configSections. Através dele, especificamos o nome da seção que será definido para estruturarmos o XML. Além disso, é necessário informar o tipo que será utilizado (que na verdade é a classe UrlsSection que criamos acima) e também o Assembly em qual ela se encontra. Para exemplificar a configuração, vamos analisar o código abaixo: <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="urlsSection" type="Aplicacao.UrlsSection, Aplicacao" /> </configSections> </configuration> Como podemos ver, dentro do elemento configSections criamos uma nova seção através do elemento section. Primeiramente precisamos informar o nome que essa seção terá dentro do arquivo de configuração. Aqui você pode escolher o que achar mais viável para

Page 121: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 28

Israel Aece | http://www.projetando.net

28

a sua aplicação mas, lembre-se que é a partir dela que irá recuperar as informações no seu código. Em seguida, utilizamos o atributo type para espeficiar que essa seção irá ser baseada em um tipo que criamos no código. Esse tipo deve ser informado com o seu full-name, ou seja, desde o namespace raiz até a classe que será utilizada. Além disso, ainda é necessário dizer qual é o Assembly em que o mesmo está contido que, no nosso caso, chama-se Aplicacao. Para complementar o arquivo de configuração, necessitamos agora incluir a seção customizada que criamos anteriormente. Como nomeamos essa seção como urlsSection, é ela que será utilizada para configurarmos todas as propriedades que criamos via código. A começar pela propriedade Name e também uma propriedade Simple, que armazena um elemento individual do tipo UrlConfigElement. Para finalizar, temos a propriedade Urls que é uma coleção e a sua estrutura é um pouco diferenciada, justamente porque armazena vários elementos que, no nosso caso, é do tipo UrlConfigElement. Abaixo temos o arquivo de configuração na íntegra: <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="urlsSection" type="Aplicacao.UrlsSection, Aplicacao" /> </configSections> <urlsSection name="Favoritos"> <simple name="Microsoft" url="http://www.microsoft.com" port="0" /> <urls> <clear /> <add name="People" url="http://www.people.com.br" port="0" /> <add name="Projetando.NET" url="http://www.projetando.net/" port="8080" /> </urls> </urlsSection> </configuration> Como podemos notar, temos o elementos urls qual podemos adicionar quantos elementos desejarmos, pois trata-se de uma coleção. Agora, para acessarmos essas informações dentro da aplicação, é necessário utilizarmos a classe ConfigurationManager que

Page 122: Livro de .NET - Israel Aece

Capítulo 3 – Utilização de Assemblies 29

Israel Aece | http://www.projetando.net

29

fornece vários métodos estáticos, que permitem manipular o arquivo de configuração da aplicação corrente. Um método importante que a classe ConfigurationManager disponibiliza é o método OpenExeConfiguration que retorna um objeto do tipo Configuration que representa as configurações que estão no arquivo de configuração. Mas no nosso caso, utilizaremos uma espécie de atalho. Trata-se de um outro método, chamado GetSection que, dado uma string com o nome da seção, ele retorna a instância da mesma. O código abaixo mostra como resgatar as informações do arquivo de configuração e, para fins de exemplo, vamos apenas mostrá-los na tela: VB.NET Imports System.Configuration Dim section As UrlsSection = DirectCast(ConfigurationManager.GetSection("urlsSection"), UrlsSection) Console.WriteLine(section.Name) For Each element As UrlConfigElement In section.Urls Console.WriteLine(element.Name & " - " & element.Url) Next C# using System.Configuration; UrlsSection sec = ConfigurationManager.GetSection("urlsSection") as UrlsSection; Console.WriteLine(sec.Name); foreach (UrlConfigElement element in sec.Urls) Console.WriteLine(element.Name + " - " + element.Url); O quadro abaixo exibe o output deste código: Favoritos People - http://www.people.com.br Projetando.NET - http://www.projetando.net/

Page 123: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 1

Israel Aece | http://www.projetando.net

1

Capítulo 4 Monitoramento e depuração de aplicações Introdução Como o Visual Studio .NET é a principal ferramenta para o desenvolvimento de aplicações .NET, não seria completa se não tivesse várias funcionalidades em sua IDE que permite a fácil depuração e monitoramento das aplicações. A utilização do depurador do Visual Studio .NET tem duas vantagens:

1. fornece uma interface familiar que permite o acompanhamento passo-à-passo do código/processo.

2. tem um forte integração com o Visual Studio .NET. O Visual Studio .NET em conjunto com o seu depurador fornecem uma gama de ferramentas que dão uma grande flexibilidade para a depuração completa de uma aplicação, seja ela Windows, Web ou qualquer tipo de serviço. Além das funcionalidades que já estão embutidas, elas também são extensíveis, o que permite os desenvolvedores customizarem, extendendo alguns aspectos e funcionalidades para ter uma melhor visualização de problemas mais específicos. A primeira parte do capítulo trata de como manipular o Event Log do Windows, que é um repositório de informações referentes as mais diversas aplicações que são executadas em cima do sistema operacional. Veremos como criar repositórios específicos para a nossa aplicação dentro dele, bem como logar informações dentro dela. Logo em seguida, analisaremos como manipular os processos que estão sendo executados na máquina em que a aplicação é executada. Já na segunda parte, analisaremos grande parte das funcionalidades que a IDE do Visual Studio .NET fornece para ajudar na depuração da aplicação e também como extendê-la com um exemplo simples. Finalmente, veremos como habilitar o Tracing dentro da aplicação, que permite acompanhar o comportamento da mesma durante os testes, bem como quando estiver em produção, para detectar possíveis falhas. Windows Event Log Quando um problema ocorre o administrador do sistema ou técnicos que fornecem suporte devem ter informações a respeito da falha, quem gerou, quando, etc.. Muitas aplicações persistem erros e eventos de diversas formas, mantendo cada uma, um padrão proprietário que satisfaz a necessidade dela. Esses padrões proprietários possuem diferentes informações, formatos e disponibilizam, também de forma diferente, para o usuário. O Event Log do Windows fornece uma forma centralizada e padronizada para que as aplicações (e também para o sistema operacional) possa salvar os eventos e erros que desejarem.

Page 124: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 2

Israel Aece | http://www.projetando.net

2

Felizmente o próprio Windows fornece uma interface que permite aos usuários visualizarem os possíveis problemas e eventos que ocorreram em uma determinada aplicação. Essa interface chama-se Event Viewer e pode ser visualizada através da imagem abaixo:

Imagem 4.1 – Interface do Event Viewer do Windows

Tipos de Logs O .NET Framework já traz embutido diversas classes que auxiliam a manipulação total do Event Log do Windows. Essas classes fornecem toda a infraestrutura para a criação de novos repositórios como a gravação de eventos dentro deles. Atualmente existem cinco tipos de eventos que podem ser logados. A tabela abaixo descreve cada um desses tipos: Tipo de Evento Descrição Error Um evento que indica um problema grave, como por exemplo, uma

falha ocorreu quando um serviço tentou ser carregado. Warning Um evento que não é uma falha, mas pode representar um futuro

problema. Information Um evento que tem a finalidade de logar informações a nível de

sucesso, como por exemplo, serviço foi iniciado, serviço foi parado. Success Audit Um evento que indica que a auditoria de segurança foi efetuada com

sucesso. Failure Audit Um evento que indica que a auditoria de segurança falhou. Quando você não informa nenhum dos tipos de eventos que vimos acima, por padrão, ele assumo o tipo Information. Computadores que rodam sistema operacional Windows 2000 ou superior possuem 3 tipos de event logs:

1. System Log: armazena eventos que ocorrem nos componentes do sistema, como por exemplo um problema com um driver qualquer.

2. Security Log: armazena eventos com relação à segurança. 3. Application Log: armazena eventos que ocorrem em uma aplicação

Page 125: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 3

Israel Aece | http://www.projetando.net

3

Arquitetura e manipulação fornecida pelo .NET O .NET Framework fornece um namespace chamado System.Diagnostics. É dentro dele que temos todas as classes necessárias para manipular o Event Log do Windows. Este namespace vai muito além disso, fornecendo classes para tracing e monitoramento das aplicações que veremos mais tarde, ainda neste capítulo. Basicamente temos três tipos principais que são utilizados para a manipulação do Event Log: EventLog, EventLogEntryType e EventLogEntry. A classe EventLog é responsável por interagir entre a aplicação e o Event Log do Windows, customizando para que a sua aplicação possa gravar os eventos relevantes dentro dele. A partir dele também podemos, além de criar logs e entradas, ler todas as entradas de um log, bem como excluir os logs. Entendam por log o repositório que armazena todas as entradas feitas pelas aplicações. Essa é uma classe que fornece uma porção de métodos estáticos que permitem interagir com o Event Log. Para exemplificar a criação de um novo log, vamos analisar o código abaixo: VB.NET Imports System.Diagnostics If Not EventLog.SourceExists("AplicacaoWeb") Then EventLog.CreateEventSource("AplicacaoWeb", "People") End If If Not EventLog.SourceExists("AplicacaoWin") Then EventLog.CreateEventSource("AplicacaoWin", "People") End If Dim logWeb As New EventLog() logWeb.Source = "AplicacaoWeb" logWeb.WriteEntry("Log - Web App.") Dim logWin As New EventLog() logWin.Source = "AplicacaoWin" logWin.WriteEntry("Log - Win App.") C# using System.Diagnostics; if(!EventLog.SourceExists("AplicacaoWeb")) EventLog.CreateEventSource("AplicacaoWeb", "People"); if(!EventLog.SourceExists("AplicacaoWin")) EventLog.CreateEventSource("AplicacaoWin", "People");

Page 126: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 4

Israel Aece | http://www.projetando.net

4

EventLog logWeb = new EventLog(); logWeb.Source = "AplicacaoWeb"; logWeb.WriteEntry("Log - Web App."); EventLog logWin = new EventLog(); logWin.Source = "AplicacaoWin"; logWin.WriteEntry("Log - Win App."); Antes de analisar o código, é necessário termos em mente dois termos importantes: Log e Source. O primeiro deles, Log, é o local onde armazenaremos os eventos, ou seja, o repositório dos eventos. Já o segundo, Source, é quem (aplicação/serviço) gerou o evento. Neste momento nos deparamos com um método chamado WriteEntry que é responsável por encapsular a escrita de uma entrada dentro do Event Log. No exemplo acima, utilizamos um dos overloads do método que é disponibilizado, ou seja, somente passando a mensagem que será gravada no Log. Dentre vários overloads que este método fornece, temos ainda um que recebe como parâmetro, além da mensagem, um enumerador do tipo EventLogEntryType que, nada mais é que o tipo de evento (Error, Warning, Information, Success Audit e Failure Audit) e, como não informamos nenhum tipo no exemplo acima, por padrão, ele assume Information. Como podemos visualizar através do código acima, criamos um Log para a empresa People. Todas as aplicações da People vão logar as informações dentro deste repositório. Depois disso, cada aplicações tem o seu identificador, o Source, que identificará dentro do Event Log quem foi o responsável pela geração do evento. Através da imagem abaixo você pode analisar os eventos recém criados e com o Source já especificado (em vermelho):

Imagem 4.2 – Event Log já com as entradas.

Se quisermos apagar via aplicação tanto o Log quanto o Source também é possível através da classe EventLog, como é mostrado abaixo:

Page 127: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 5

Israel Aece | http://www.projetando.net

5

VB.NET Imports System.Diagnostics EventLog.DeleteEventSource("AplicacaoWeb") EventLog.DeleteEventSource("AplicacaoWin") EventLog.Delete("People") C# using System.Diagnostics; EventLog.DeleteEventSource("AplicacaoWeb"); EventLog.DeleteEventSource("AplicacaoWin"); EventLog.Delete("People"); Cada entrada que adicionamos vão sendo colocadas dentro do Event Log. Como vimos um pouco mais acima, além dos métodos estáticos que a classe EventLog disponibiliza, ela fornece outras funcionalidades, pois a partir de uma instância dela que temos acesso ao Log como um todo, permitindo interagir com este Log, gravando entradas, lendo essas entradas, etc.. O exemplo abaixo mostra como devemos fazer para exibir os eventos que foram previamente depositados dentro do Event Log do Windows: VB.NET Imports System.Diagnostics Dim logs As New EventLog("People") For Each log As EventLogEntry In logs.Entries Console.WriteLine(log.Message) Next C# using System.Diagnostics; EventLog logs = new EventLog("People"); foreach(EventLogEntry log in logs.Entries) Console.WriteLine(log.Message); Neste momento temos nos deparamos com uma nova classe do tipo EventLogEntry. Esta classe representa uma entrada individual que está dentro do Event Log. A única finalidade desta classe é mesmo encapsular todo o acesso à um determinado registro que está dentro do Event Log e, sendo assim, não é permitido criar instâncias da mesma. A forma de trabalhar com ela é mostrado acima, ou seja, quando iteramos através da

Page 128: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 6

Israel Aece | http://www.projetando.net

6

propriedade Entries do objeto EventLog, cada elemento que a coleção retorna é do tipo EventLogEntry e, entre as principais propriedades que ela expõe, temos: Propriedade Descrição EntryType Retorna um enumerador do tipo EventLogEntryType que indica qual o

tipo da entrada. Index Retorna um número inteiro indicando qual o índice que a entrada ocupa

dentro do Log. InstanceId Retorna um número que identifica unicamente a entrada dentro do

Source. Message Recupera a mensagem. Source Recupera o nome da aplicação que gerou o evento. UserName Recupera o nome do usuário que é responsável pelo evento. Nota: As aplicações ASP.NET também suportam o acesso a Event Logs e fornecem uma arquitetura flexível para escolhermos onde desejamos persistir os eventos. Esse recurso chama-se Health Monitoring que está fora do escopo desta curso. Processos Imagine que a sua aplicação gera arquivos em formato texto que salvam diversos tipos de informações. Em algum local, dentro desta mesma aplicação, você lista todos os arquivos gerados e disponíveis para que o usuário possa visualizar. Para poupar trabalho do usuário que precisaria conhecer o caminho até o arquivo para abrí-lo no bloco de notas, automatizamos este passo, ou seja, quando o usuário dar um duplo clique em cima do arquivo, o notepad é aberto automaticamente, passando para o mesmo o arquivo texto a ser exibido. É neste momento que um novo processo é criado. O .NET Framework fornece, também dentro do namespace System.Diagnostics, uma classe chamada Process que permite extrair informações dos processos que estão sendo executados dentro de um determinado computador. Dentro do Windows, você pode ver todos os processos que estão sendo executados no momento através da Windows Task Manager, quais podemos também acessá-los dentro de uma aplicação .NET. Para fins de exemplo, vamos utilizar o bloco de notas, mas nada impede de inicializarmos aplicações mais complexas, como por exemplo, o Word, Excel, etc.. A classe Process fornece acesso para os processos que estão sendo executados no computador. Essa classe permite-nos controlar um determinado processo, iniciando, parando e para fins de monitoramento da aplicação. A classe Process também fornece vários métodos estáticos para a manipulação do de um determinado processo, que permite inicializar diretamente, sem a necessidade de uma instância da classe Process. Entre os principais métodos, temos: Métodos Estáticos Descrição GetCurrentProcess Retorna um objeto do tipo Process contendo todas as informações

Page 129: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 7

Israel Aece | http://www.projetando.net

7

do projeto corrente. GetProcessById Dado um número inteiro que representa o identificar de um

processo, retorna um objeto do tipo Process que representa o processo. O Id que ele espera como parâmetro é o PID (Process Identifier), qual pode ser visualizado através da barra de tarefas do Windows, como é mostrado através da imagem abaixo:

Imagem 4.3 – Barra de Tarefas do Windows

GetProcesses Retorna um array de objetos do tipo Process que representam todos os processos que estão rodando no computador.

Start Permite inicializar diretamente um processo, sem a necessidade de criar um objeto do tipo Process.

Logo, para inicializarmos o bloco de notas, podemos optar por duas formas: a primeira é utilizar o método estático Start. Já a segunda, é através de uma instância de uma classe do tipo Process. A segunda te possibilita uma maior controle sobre a mesma se precisar mais tarde recuperar informações a respeito do processo. O código abaixo exibe duas formas equivalentes de inicializar o bloco de notas: VB.NET Imports System.Diagnostics ‘Primeira forma: Process.Start("notepad.exe", "Arquivo.txt") ‘Segunda forma: Dim p As New Process() p.StartInfo.FileName = "notepad.exe" p.StartInfo.Arguments = "Arquivo.txt" p.Start() C# using System.Diagnostics; //Primeira forma: Process.Start("notepad.exe", "Arquivo.txt");

Page 130: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 8

Israel Aece | http://www.projetando.net

8

//Segunda forma: Process p = new Process(); p.StartInfo.FileName = "notepad.exe"; p.StartInfo.Arguments = "Arquivo.txt"; p.Start(); Depois de inicializado o processo, se em algum momento quisermos finalizar o mesmo, utilizamos o método Kill, que força a finalização do processo. Como esse método é executado assincronamente, em seguida invoque o método WaitForExit para aguardar o processo ser finalizado, se assim desejar. Agora, se desejar listar todos os processos que estão sendo executados atualmente na máquina, então pode optar pela utilização do método GetProcesses, assim como é mostrado abaixo: VB.NET Imports System.Diagnostics For Each p As Process In Process.GetProcesses() Console.WriteLine(String.Format("Processo: {0} – Id: {1}", _ p.ProcessName, _ p.Id)) Next C# using System.Diagnostics; foreach(Process p in Process.GetProcesses()) { Console.WriteLine(String.Format("Processo: {0} – Id: {1}", p.ProcessName, p.Id)); } Depurando uma aplicação Sempre que estamos desenvolvendo aplicações, independente de qual linguagem ou plataforma estamos utilizando, há sempre uma necessidade muito grande de podemos depurá-la enquanto o processo de desenvolvimento acontece. A depuração consiste em conseguirmos encontrar possíveis falhas dentro da nossa aplicação e, entre essas falhas, temos basicamente três tipos: sintaxe, runtime e lógica.

Page 131: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 9

Israel Aece | http://www.projetando.net

9

A primeira falha, a de sintaxe, que a mais fácil de ser identificada, acontece quando escrevemos algum tipo de código que o compilador não consegue entender e já acusa o problema antes mesmo da aplicação ser executada. Em segundo lugar, temos as falhas que ocorrem em tempo de execução que, se não for tratada, muitas vezes força a aplicação a ser fechada e, para citar alguns exemplos temos: acesso a algum componente, servidor de banco de dados inexistente, serviço indisponível, etc.. Finalmente, temos as falhas de lógica que são um pouco mais difícies de encontrar, já que o compilador não consegue antecipar a falha e ela provavelmente irá explodir quando tal código for executado. Alguns exemplos desse tipo de falha são: incompatibilidade de tipos, divisão por zero, objeto inexistente, dados inválidos, etc.. Com todos esses possíveis problemas que todos nós desenvolvedores estamos acostumados a enfrentar, seria terrível se não tivéssemos uma ferramenta eficaz para nos ajudar a encontrar o problema que está acontecendo. Felizmente o Microsoft Visual Studio .NET 2005 fornece uma grande ferramenta de debugger que auxilia na depuração de qualquer projeto que está sendo nele construído, permitindo diagnosticar e consertar rapidamente o problema que está ocorrendo. O debugger permite você analisar e modificar valores de variáveis, visualizar a StackTrace, threads, dumps de memória e muito mais. Além disso, possui Intellisense em algumas janelas usadas exclusivamente para depuração e, apesar de fortemente integrado com o Visual Studio 2005, não deixa de ser flexível e permite que nós desenvolvedores estendam as funcionalidades e customizamos alguns controles para facilitar ainda mais o processo de depuração de código. Quando você desenvolve uma aplicação, no Visual Studio .NET você trabalha em dois modos: modo de desenvolvimento e modo de execução. Quando estamos em modo de desenvolvimento, você pode criar, editar o código e também detectar possíveis problemas de sintaxe, que a própria IDE lhe ajudará a resolver, como por exemplo é o caso do Visual Basic .NET:

Page 132: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 10

Israel Aece | http://www.projetando.net

10

Imagem 4.4 – Auxiliar que nos sugere as possibilidades que existem para ajustar o

código

Agora, problemas de lógica dificilmente você detecta durante esse modo. Somente quando a aplicação for executada e você ver que o resultado não é o esperado é que você detecta um possível problema mas, não pode mudar nenhuma linha de código neste momento. Neste momento é que aparece um novo modo, que chamamos de break mode. Esse modo permite-nos para a aplicação em um determinado ponto para que seja possível acompanhar a executação via linha de código e, conseqüentemente, analisar tudo o que acontece nos “bastidores” da aplicação. Para entrar neste modo, você precisa colocar um breakpoint na linha que deseja que o depurador para que assim você possa acompanhar. Através da tecla F5 ou mesmo via menu Debug, Start Debbuing você também pode iniciar a aplicação anexando a mesma, o depurador. Com a aplicação neste modo, você já conseguirá averiguar o código que escreveu e acompanhar a execução passo à passo do mesmo. Nas versões anteriores do Visual Studio .NET tínhamos os data tips. Os data tips serviam para quando, em break mode, passávamos o mouse por cima de uma tipo simples (inteiro, string, double, etc.) ele já exibia o valor da variável. Já o Visual Studio .NET 2005, os data tips ainda existem, mas agora muito mais poderosos, pois permitem também a visualização de objetos mais complexos, podendo visualizar todas as suas propriedades. Já no caso das coleções, os elementos também podem ser visualizados simplesmente passando o cursor do mouse em cima, assim como é mostrado na imagem abaixo:

Page 133: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 11

Israel Aece | http://www.projetando.net

11

Imagem 4.5 – Data tips para objetos complexos

Criando um Debug Visualizer A Microsoft incluiu na versão 2.0 do .NET Framework uma possibilidade de criar um debugger customizado para um determinado objeto que temos. Isso permite-nos criar uma interface mais amigável em relação à qual é fornecida quando utilizamos o debug do Visual Studio .NET 2005. Essa técnica é chamada de Debugger Visualizer. Você pode escrever esse visualizador para qualquer objeto da sua aplicação, com excessão de um Object ou Array. A arquitetura do debugger está dividida em duas partes:

• O debugger side corre dentro do debugger do Visual Studio .NET, podendo ser criado e exibido uma interface para seu visualizador.

• O debuggee side corre dentro do processo que o Visual Studio .NET está depurando.

O objeto que você está querendo visualizar (uma string ou Image, por exemplo) existe dentro do processo debuggee. Logo, o debuggee side deve mandar os dados para o debugger side que, por sua vez, exibe o objeto para que o desenvolvedor possa depurar. Essa visualização é criada por nós que, ainda nesse artigo, veremos como criá-la. O debugger side recebe o objeto que será visualizado através de um object provider, o qual implementa uma Interface chamada IVisualizerObjectProvider. O debuggee side

Page 134: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 12

Israel Aece | http://www.projetando.net

12

envia o objeto através de um object source, o qual deriva de uma classe chamada VisualizerObjectSource. O object provider também devolve o objeto para o object source, permitindo assim, além de exibir o objeto, editá-lo no visualizador (se assim desejar) e devolvê-lo para a aplicação. Essa comunicação é efetuada através de um objeto do tipo Stream. Para criarmos este visualizador, primeiramente precisamos marcar a classe como sendo uma classe de DebuggerVisualizer e, para isso, é fornecido um atributo chamado DebuggerVisualizer (usado a nível de Assembly ou classe) para definí-la como um visualizador. Além disso, a classe deverá obrigatoriamente herdar de uma classe abstrata chamada DialogDebuggerVisualizer e implementar o método chamado Show para customizar a visualização. O atributo DebuggerVisualizer, contido dentro do namespace System.Diagnostics, fornece mais alguns parâmetros para a configuração do visualizador. O primeiro parâmetro é tipo, ou seja, a classe derivada DialogDebuggerVisualizer que é o nosso visualizador efetivamente; já o segundo especifica o tipo de objeto que fará a comunicação entre o debugger side e o debuggee side e, se não informado, um padrão será utilizado. O restante, chamado de "Named Parameters", você deve especificar o tipo de objeto que o visualizador irá trabalhar (uma string ou Image, por exemplo) e no outro, é o nome que aparecerá dentro do Visual Studio .NET 2005, para permitir visualização. Para ilustrar, veremos abaixo o código que cria o visualizador: VB.NET Imports System Imports Microsoft.VisualStudio.DebuggerVisualizers Imports System.Windows.Forms Imports System.Drawing Imports System.Diagnostics <_ Assembly: DebuggerVisualizer(GetType(DebugTools.ImageVisualizer), _ GetType(VisualizerObjectSource), _ Target := GetType(System.Drawing.Image), _ Description := "Image Visualizer") _ > Namespace DebugTools Public Class ImageVisualizer Inherits DialogDebuggerVisualizer Protected Overrides Sub Show(IDialogVisualizerService windowService, _ IVisualizerObjectProvider objectProvider) Image data = DirectCast(objectProvider.GetObject(), Image)

Page 135: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 13

Israel Aece | http://www.projetando.net

13

Using(ImageVisualizerForm f = New ImageVisualizerForm()) f.Image = data windowService.ShowDialog(f) End Using End Sub End Class End Namespace C# using System; using Microsoft.VisualStudio.DebuggerVisualizers; using System.Windows.Forms; using System.Drawing; using System.Diagnostics; [ assembly: DebuggerVisualizer(typeof(DebugTools.ImageVisualizer), typeof(VisualizerObjectSource), Target = typeof(System.Drawing.Image), Description = "Image Visualizer") ] namespace DebugTools { public class ImageVisualizer : DialogDebuggerVisualizer { protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) { Image data = (Image)objectProvider.GetObject(); using(ImageVisualizerForm f = new ImageVisualizerForm()) { f.Image = data; windowService.ShowDialog(f); } } } } Note que foi criado um formulário chamado ImageVisualizerForm. Este formulário é encarregado de receber a imagem e exibí-la. Não há segredos nele, ou seja, apenas foi criada uma propriedade chamada Image que recebe a imagem vinda do object provider. O primeiro parâmetro do método Show, chamado windowService do tipo

Page 136: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 14

Israel Aece | http://www.projetando.net

14

IDialogVisualizerService, fornece métodos para que o visualizador possa estar exibindo formulários, controles e janelas de diálogo. Se você quiser que o seu visualizador edite o objeto e o devolva para a aplicação, terá que sobrescrever os métodos TransferData ou CreateReplacementObject da classe VisualizerObjectSource. Finalmente, para que possamos utilizar o visualizador dentro do Visual Studio .NET 2005 teremos que compilar o projeto e, com a DLL gerada, colocá-la dentro do diretório <Diretorio VS.NET>\Common7\Packages\Debugger\Visualizers. Quando abrir o Visual Studio, já terá acesso ao visualizador, assim como é mostrado através da imagem abaixo:

Imagem 4.6 – Utilizando Debug Visualizer customizado

Ainda existem outros atributos como o DebuggerVisualizer. Da mesma forma que o visualizador, Ainda dentro do namespace System.Diagnostics temos outras classes importantes que auxiliar no processo de depuração de código. Essas classes são: Debugger, StackFrame e StackTrace. A primeira delas, Debugger, trata-se de uma classe que permite, programaticamente, efetuar a comunicação com o debugger do Visual Studio .NET 2005. Essa classe é comumente utilizada quando você precisa depurar uma seção de código que já foi executada por algum outro processo, ou ainda, quando esse código é disparado sem antes mesmo da aplicação inicializar. Essa classe alguns membros estáticos que veremos a seguir as suas funcionalidades: Membro Descrição IsAttached Retorna um valor booleano indicando se debugger está ou não anexado

ao processo. Break Quando chamado este método, ele gera um breakpoint e,

conseqüentemente, a aplicação entra em break mode e assim você consegue depurar o código.

Page 137: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 15

Israel Aece | http://www.projetando.net

15

IsLogging Indica se o log está ou não habilitado para o debugger. Launch Quando o debugger não estiver anexado ao processo, você pode invocar

esse método para lançar o debugger e anexá-lo ao processo. Log Insere uma mensagem no debugger corrente. A classe StackFrame, apesar de ser pública, é utilizada pela infraestrutura do .NET Framework e não é indicado utilizá-la diretamente no seu código. Stack frame trata-se da representação física de uma chamada à alguma função dentro da thread corrente. Esse objeto é criado e colocado dentro da pilha de chamadas de funções que são realizadas em toda a execução da thread. A classe StackTrace é basicamente uma coleção de StackFrames. A classe Exception fornece uma propriedade chamada StackTrace do tipo string. Quando alguma exceção acontece no código, essa propriedade retornará todas as chamadas realizadas as funções até que onde o problema aconteceu. Isso ajudará imensamente você a detectar onde o problema realmente se encontra. Janelas de depuração Quando iniciamos o depurador do Visual Studio .NET, várias janelas ficam disponíveis para nos auxiliar durante o processo de depuração. Essas janelas ficam disponíveis no menu Debug, Windows. Cada uma delas fornece um funcionalidade diferente, a seguir: Janela Descrição Breakpoint Exibe uma janela onde você pode visualizar todos os breakpoint

definidos na aplicação e também fornece recursos para excluí-los ou desabilitá-los.

Output Exibe informações de status para várias features da IDE do Visual Studio .NET.

Script Explorer Exibe uma lista de arquivos de scripts que estão atualmente carregados no programa que você está depurando.

Watch Cria uma lista customizada de variáveis e expressões que deseja monitorar.

Autos Visualiza todas as variáveis dentro bloco corrente e de três blocos antes e três blocos depois do bloco corrente.

Locals Permite visualizar e modificar o valor das variáveis locais. Immediate Utilizado para depurar e avaliar expressões, executar código,

imprimir valores de variáveis, entre outros, a partir de linha de comando.

Call Stack Visualiza todo o histórico de chamadas das linhas de código que estão sendo depuradas.

Threads Exibe e controla as threads do programa que está sendo depurado. Modules Exibe uma lista de módulos (DLL ou EXE) que estão sendo

utilizadas pelo programa que está sendo depurado, mostrando informações relevantes sobre cada uma delas.

Page 138: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 16

Israel Aece | http://www.projetando.net

16

Processes Exibe uma lista com todos os processos que você tem anexado ou lançado a partir do Visual Studio .NET.

Memory Disponibiliza informações e permite a visualização do espaço de memória que está sendo utilizado pela sua aplicação.

Disassembly Esta janela mostra o código assembly correspondente ao código, criado pelo compilador.

Tracing Tracing é a forma que temos para monitorar a execução da aplicação enquanto ela está rodando. Em tempo de desenvolvimento, você pode adicionar instrumentações de tracing e debugging para a aplicação .NET e, poderá utilizá-la durante o processo de desenvolvimento e também depois de distribuído. Para aplicações Windows, o namespace System.Diagnostics fornece duas classes para esse tipo de monitoramento: Trace e Debug. Já quando estamos falando de aplicações Web, temos a classe TraceContext dentro do namespace System.Web, que nada mais é que, entre outras funcionalidades específicas para o contexto, um wrapper para a classe Trace. Basicamente as classes Trace e Debug possuem as mesmas finalidades. A única diferença significativa entre as classes é que primeira delas, a Trace, é compilada por padrão para dentro do Assembly e a classe Debug não. Enquanto estamos desenvolvendo a aplicação, tanto as informações de Trace e Debug são exibidas na janela Output do Visual Studio .NET. Para habilitar essas funcionalidades para elas serem distribuídas juntamente com o Assembly, você deve compilar a aplicação com a diretiva TRACE ou DEBUG. Para habilitar ou desabilitar esses recursos você tem duas formas: ou via IDE ou via linha de comando:

1. Via IDE: a. Visual Basic .NET: Propriedades do Projeto Aba Compile

Advanced Compile Options Compilation Constants. b. Visual C#: Propriedades do Projeto Aba Build General.

2. Via linha de comando: a. Visual Basic .NET: vbc /r:App.dll /d:TRACE=TRUE

/d:DEBUG=FALSE Main.vb b. Visual C#: csc /r:App.dll /d:TRACE /d:DEBUG=FALSE Main.cs

O uso destas classes é bastante simples, mas antes de visualizar como utilizá-las, vamos analisar os métodos estáticos que elas fornecem e para que serve cada um deles. Logo em seguida, temos um trecho de código que basicamente mostra os métodos sendo invocados a partir de suas respectivas classes. Método Descrição Assert Dado uma condição, ele analisa se a mesma é ou não falsa. Se for, a

mensagem especificada é armazenada e, se estiver rodando uma aplicação que possui uma interface gráfica, uma caixa de diálogo é

Page 139: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 17

Israel Aece | http://www.projetando.net

17

exibida com mensagem. WriteIf Dado uma condição, ele analisa se a mesma é ou não verdadeira. Se for, a

mensagem especificada é armazenada. Fail Grava a mensagem especificada e exibe uma caixa de diálogo é exibida

com a mesma mensagem. Write Simplesmente salva a mensagem. WriteLine Salva mensagem, adicionando uma quebra de linha. WriteLineIf Dado uma condição, ele analisa se a mesma é ou não verdadeira. Se for, a

mensagem especificada é armazenada juntamente com uma quebra de linha.

VB.NET Imports System.Diagnostics Trace.Assert(1 > 2, "1 nunca será maior que 2") Debug.Assert(1 > 2, "1 nunca será maior que 2") Trace.WriteIf(1 = 1, "1 é igual a 1") Debug.WriteIf(1 = 1, "1 é igual a 1") Trace.Fail("Algo aconteceu.") Debug.Fail("Algo aconteceu.") Trace.Write("Um valor para logar.") Debug.Write("Um valor para logar.") Trace.WriteLine("Um valor para logar c/ quebra de linha.") Debug.WriteLine("Um valor para logar c/ quebra de linha.") Trace.WriteLineIf(1 = 1, "1 é igual a 1 c/ quebra de linha.") Debug.WriteLineIf(1 = 1, "1 é igual a 1 c/ quebra de linha.") C# using System.Diagnostics; Trace.Assert(1 > 2, "1 nunca será maior que 2"); Debug.Assert(1 > 2, "1 nunca será maior que 2"); Trace.WriteIf(1 = 1, "1 é igual a 1"); Debug.WriteIf(1 = 1, "1 é igual a 1"); Trace.Fail("Algo aconteceu."); Debug.Fail("Algo aconteceu."); Trace.Write("Um valor para logar."); Debug.Write("Um valor para logar."); Trace.WriteLine("Um valor para logar c/ quebra de linha.");

Page 140: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 18

Israel Aece | http://www.projetando.net

18

Debug.WriteLine("Um valor para logar c/ quebra de linha."); Trace.WriteLineIf(1 = 1, "1 é igual a 1 c/ quebra de linha."); Debug.WriteLineIf(1 = 1, "1 é igual a 1 c/ quebra de linha."); Até o momento essas configurações nos atendem. Mas nem sempre temos o Visual Studio .NET e sua janela de Output nos clientes que iremos instalar a aplicação. Seria muito mais conveniente neste caso, podemos persistir tais informações em arquivos textos ou até mesmo no Event Log do Windows, se assim desejarmos. Além disso, pode surgir a necessidade de filtrar qual tipo de mensagem desejamos persistir e, nos exemplos acima tudo, sem excessão, é salvo. É neste momento que entra em ação outros objetos que são utilizados para fornecer uma maior flexibilidade para configurar e dar manutenção no tracing da aplicação. Nesta seção veremos os seguintes elementos: TraceSwitch, TraceListener e TraceSource. TraceSwitch Switches permitem você habilitar, desabilitar e filtrar as mensagens de tracing, determinando se elas devem ou não serem persistidas. São objetos que estão acessíveis via código, mas que também podem ser configurados via arquivos de configuração, o que dá uma maior flexibilidade, já que possíveis alterações não exigirá que se recompile o programa. Atualmente existem três tipos de switches fornecidos pelo .NET Framework: BooleanSwitch, TraceSwitch e SourceSwitch. O primeiro deles possibilita ligar e desligar o tracing. Já os dois últimos, permitem você habilitar ou desabilitar o switch para um determinado nível. Esse switch baseia-se nos seguintes níveis, disponibilizados pelo enumerador TraceLevel: Error, Warning, Info e Verbose. São esses níveis que são utilizados como filtros para configurarmos dentro da aplicação. Podemos em algum momento, queremos filtrar somente as mensagens de nível Error e, mais tarde, somente o que for Warning. Através da tabela abaixo conseguimos visualizar de forma tabular o que cada um os níveis possibilita: Item Constante Tipo de Mensagem Off 0 Nenhuma. Error 1 Somente mensagens de erro. Warning 2 Mensagens de aviso e mensagens de erro. Info 3 Mensagens informativas, de aviso e erro. Verbose 4 Mensagens verbose, informativas, de aviso e erro.

Verbose: exibe toda e qualquer mensagem de tracing e debugging.

Page 141: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 19

Israel Aece | http://www.projetando.net

19

Inicialmente vamos analisar a implementação de código para o switch BooleanSwitch onde, via arquivo de configuração, vamos ver como devemos proceder para habilitar e desabilitar o switch, sem a necessidade de recompilar o código. Inicialmente, analisaremos a configuração que deve ser realizada no arquivo de configuração da aplicação para suportar o swtich. O código a seguir é idêntico para qualquer uma das linguagens: App.Config <configuration> <system.diagnostics> <switches> <add name="ModuloDados" value="0"/> </switches> </system.diagnostics> </configuration> No atributo name definimos o nome do switch. Esse nome é o mesmo que deve ser referenciado dentro da aplicação, para vincular o switch ao arquivo de configuração. O atributo value indica se o switch está ou não habilitado, ou seja, 0 (zero) está desabilitado e 1 (um) está habilitado. O código abaixo como fazer o vinculo entre o arquivo de configuração e a aplicação e, reparem que o mesmo nome é passado para o construtor da classe BooleanSwitch: VB.NET Imports System.Diagnostics Private dataSwitch As New _ BooleanSwitch("ModuloDados", "Acesso a dados.") Sub Main() If dataSwitch.Enabled Then Console.WriteLine("Habilitado.") Else Console.WriteLine("Não habilitado.") End If End Sub C# using System.Diagnostics; private static BooleanSwitch dataSwitch = new BooleanSwitch("ModuloDados", "Acesso a dados."); static void Main(string[] args) {

Page 142: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 20

Israel Aece | http://www.projetando.net

20

if (dataSwitch.Enabled) { Console.WriteLine("Habilitado."); } else { Console.WriteLine("Não habilitado."); } } Já a utilização do TraceSwitch também é bem semelhante, pois também permite a configuração via App.Config. Neste caso, no arquivo de configuração vamos informar qual o tipo de mensagem de tracing que desejamos que seja persistida, filtrando a partir de nível de mensagem, mais precisamente, através do enumerador TraceLevel, qual já analisamos mais acima. Essa classe possui quatro propriedades, sendo uma para cada nível do enumerador TraceLevel: TraceError, TraceInfo, TraceVerbose e TraceWarning, onde cada uma retorna um valor booleano indicando se o switch pode ou não persistir informações de um determinado nível. Portanto, no arquivo de configuração devemos informar um dos itens do enumerador, podendo ser a constante (número inteiro) ou o “alias”. O código abaixo exemplifica o uso do switch TraceSwitch: App.Config <configuration> <system.diagnostics> <switches> <add name="ModuloDados" value="Warning"/> </switches> </system.diagnostics> </configuration> VB.NET Imports System.Diagnostics Private dataSwitch As New _ TraceSwitch("ModuloDados", "Acesso a dados.") Sub Main() Console.WriteLine("Error: {0}", dataSwitch.TraceError) Console.WriteLine("Warning: {0}", dataSwitch.TraceWarning) End Sub C#

Page 143: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 21

Israel Aece | http://www.projetando.net

21

using System.Diagnostics; private static TraceSwitch dataSwitch = new TraceSwitch("ModuloDados", "Acesso a dados."); static void Main(string[] args) { Console.WriteLine("Error: {0}", dataSwitch.TraceError); Console.WriteLine("Warning: {0}", dataSwitch.TraceWarning); } Se repararem no atributo value do arquivo de configuração, temos ali definido Warning (podendo ser a constante 2 que teria o mesmo efeito). Como esses itens são acumulativos, o Warning irá gravar tanto informações de erros quanto de avisos. O último, porém não menos importante, é o SourceSwitch. Trata-se de uma nova classe que foi incluída na versão 2.0 do .NET Framework. Esse switch é utilizado em conjunto com o objeto TraceSource, qual veremos mais abaixo. A classe SourceSwitch possui uma propriedade importante chamada Level. Essa propriedade recebe um valor fornecido pelo enumerador SourceLevels. Os valores definidos por esse enumerador identificará quais eventos serão capturados pelo tracing. Veremos mais detalhes sobre esse switch mais abaixo, quando abordarmos a respeito da classe TraceSource. TraceListener Logo no começo desta seção, vimos que todas as informações depositadas através da classe Trace e também da classe Debug são enviadas e exibidas na janela Output do Visual Studio .NET. Isso ajuda quando estamos em tempo de desenvolvimento, mas não resolve quando a aplicação é instalada no cliente. É necessário definirmos algum lugar que permita persistir fisicamente os dados para uma posterior análise. Felizmente temos a nossa disposição os listeners. Os listeners são responsáveis por coletar, armazenar e direcionar as informações de tracing. Tanto a classe Trace quanto a classe Debug possuem uma propriedade chamada Listeners que expõem uma coleção do tipo TraceListenerCollection, que permite adicionarmos quantos listeners forem necessários. Sendo assim, podemos ter as mensagens de tracing sendo persistidas de diversas formas. Atualmente dentro do .NET Framework temos alguns listeners a nossa disposição. Todos os listeners herdam diretamente da classe abstrata TraceListener. Abaixo temos uma tabela que descreve todos os listeners que estão contidos dentro do .NET Framework: Listener Descrição TextWriterTraceListener Redireciona toda a saída para um stream, persistindo o

Page 144: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 22

Israel Aece | http://www.projetando.net

22

conteúdo dentro de um arquivo texto convencional EventLogTraceListener Redireciona toda a saída para o Event Log do Windows. DefaultTraceListener Esse é o listener padrão que é adicionado a classe Trace e

Debug. Os métodos Write e WriteLine enviam o conteúdo para a janela Output do Visual Studio .NET e também para o método Log do classe Debugger (discutida mais acima).

ConsoleTraceListener Redireciona toda a saída para a console. DelimitedListTraceListener Redireciona toda a saída para um stream, persistindo o

conteúdo dentro de um arquivo texto mas, neste caso, utilizando um delimitador. O delimitador pode ser definido através da propriedade Delimiter.

XmlWriterTraceListener Redireciona toda a saída para um stream, persistindo o conteúdo dentro de um arquivo em formato XML.

Todos os listeners podem ser configurados via código (Visual Basic .NET/Visual C#) ou, como já era de se esperar, via arquivo de configuração. Como temos a propriedade Listeners, que expõe uma coleção, podemos adicionar quantos quisermos, ou seja, podemos ter as informações de tracing persistidas em arquivos XML e arquivos textos convencionais. Nota: Todos os listeners possuem um método chamado Flush e outro chamado Close. Quando você invoca os métodos WriteXXX na verdade as informações estão sendo armazenadas na memória. Para você persistir os dados fisicamente, então terá que invocar o método Flush que tem a finalidade de gravar os dados em disco para todos os listeners ou o método Close que, além de persistir os dados para todos os listeners, também os fecha. Finalmente, vamos analisar a implementação dos listeners em código e, em seguida, via arquivo de configuração. Reparem que o método Clear da classe Trace se encarrega de limpar todos os listeners existentes na coleção e, conseqüentemente, o listener padrão: VB.NET Imports System.Diagnostics Trace.Listeners.Clear() Trace.Listeners.Add(New XmlWriterTraceListener("Log.xml")) Trace.Listeners.Add(New TextWriterTraceListener("Log.txt")) Trace.WriteLine("Teste de Tracing", "Testes") Trace.WriteLine("Adicionando uma nova entrada", "Testes") Trace.Flush()

Page 145: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 23

Israel Aece | http://www.projetando.net

23

C# using System.Diagnostics; Trace.Listeners.Clear(); Trace.Listeners.Add(new XmlWriterTraceListener("Log.xml")); Trace.Listeners.Add(new TextWriterTraceListener("Log.txt")); Trace.WriteLine("Teste de Tracing", "Testes"); Trace.WriteLine("Adicionando uma nova entrada", "Testes"); Trace.Flush(); Já abaixo temos a configuração dos listeners via arquivo de configuração, o que da uma maior flexibilidade, já que podemos adicioná-los ou removê-los, plug and play. Lembrando que a configuração é válida para qualquer uma das linguagens (Visual Basic .NET ou Visual C#): App.Config <configuration> <system.diagnostics> <trace autoflush="true" indentsize="4"> <listeners> <remove name="Default" /> <add name="txtListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="Log.txt" /> <add name="xmlListener" type="System.Diagnostics.XmlWriterTraceListener" initializeData="Log.xml" /> </listeners> </trace> </system.diagnostics> </configuration> TraceSource Esta classe foi incluída a partir da versão 2.0 do .NET Framework. Em projetos com muitos componentes, seria interessante termos um tracing diferenciado para cada um deles, o que ficaria muito mais simples de gerenciar e também uma melhor forma de organizar. Com essa necessidade, sugiu a classe TraceSource, que permite configurarmos as informações necessárias de tracing, como por exemplo os listeners, switches, etc.. O TraceSource permite montarmos toda a configuração necessária para cada seção ou componente do código que estamos desenvolvendo e, como já é previsto, além da

Page 146: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 24

Israel Aece | http://www.projetando.net

24

codificação via Visual Basic .NET ou Visual C#, permite também a configuração via App.Config, mas antes de entendermos a sua implementação, vamos analisar algums membros que essa classe fornece para a manipulação do tracing: Membro Descrição Construtor Essa classe possui dois construtores. O primeiro deles aceita

somente uma string indicando o nome do TraceSource. Já o segundo construtor, além desta string, aceita também um parâmetro do tipo SourceLevels. SourceLevels é um enumerador que especifica o nível das mensagens que serão filtradas pelo switch. Entre os possíveis valores temos:

• ActivityTracing: Permite efetuar o tracing dos seguintes eventos: Stop, Start, Suspend, Transfer e Resume.

• All: Permite o tracing de todos os eventos. • Critical: Permite somente eventos críticos. • Error: Permite eventos críticos e eventos de erros. • Information: Permite efetuar o tracing de eventos críticos,

de erros, avisos e informações. • Off: Não captura nenhum evento. • Verbose: Permite efetuar o tracing de eventos críticos, de

erros, avisos, informações e também de verbose. • Warning: Permite efetuar o tracing de eventos críticos, de

erros e avisos. Listeners Retorna a coleção de listeners. Switch Expõe ou recebe um objeto SourceSwitch. TraceData Permite escrevemos dados (de objetos e variáveis) dentro dos

listeners. Esse método recebe três parâmetros: o primeiro deles do tipo TraceEventType, que trata-se de um enumerador que define os seguintes valores:

• Critical: Erro fatal. • Error: Erro que é possível ser recuperado. • Information: Informações adicionais. • Resume: Reiniciando uma operação; • Start: Iniciando uma operação. • Stop: Parando uma operação. • Suspend: Suspendendo uma operação. • Transfer: Transferência de controle para uma outra

operação. • Verbose: Tracing de debug. • Warning: Problema não crítico.

Já o segundo parâmetro do método é do tipo inteiro, identifica o

Page 147: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 25

Israel Aece | http://www.projetando.net

25

evento e, finalmente o último parâmetro, do tipo Object que é o valor que será salvo pela switch.

TraceEvent Possui os mesmos parâmetros do método anterior, com excessão do último, que aqui é do tipo string, que permite escrever uma mensagem dentro da coleção de listeners.

TraceInformation Permite escrever uma informação adicional dentro da coleção de listeners.

TraceTransfer Escreve uma mensagem de transferência de controle de operação. Quando utilizamos os métodos TraceData e TraceEvent que esperam em seu primeiro parâmetro um valor fornecido pelo enumerador TraceEventType, o valor vai ser comparado com o valor especificado na propriedade Level da classe SourceSwitch através do enumerador SourceLevels. Veremos a partir do código abaixo a razão disso: VB.NET Imports System.Diagnostics Sub Main() Dim source As New TraceSource("Trace - Componente 1") Dim defaultSwitch As New SourceSwitch("switch") defaultSwitch.Level = SourceLevels.Critical source.Switch = defaultSwitch source.Listeners.Add(New TextWriterTraceListener("Log.txt")) source.TraceEvent(TraceEventType.Critical, 12, "Mensagem crítica") source.TraceEvent(TraceEventType.Information, 22, "Mensagem de informação") source.Close() End Sub C# using System.Diagnostics; static void Main(string[] args) { TraceSource source = new TraceSource("Trace - Componente 1"); SourceSwitch defaultSwitch = new SourceSwitch("switch"); defaultSwitch.Level = SourceLevels.Critical; source.Switch = defaultSwitch; source.Listeners.Add(new TextWriterTraceListener("Log.txt")); source.TraceEvent(TraceEventType.Critical, 12, "Mensagem crítica"); source.TraceEvent(TraceEventType.Information, 22, "Mensagem de informação"); source.Close();

Page 148: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 26

Israel Aece | http://www.projetando.net

26

} Como podemos ver, definimos SourceLevels.Critical na propriedade Level da classe SourceSwitch. Em seguida, adicionamos a instância da classe SourceSwitch na propreidade Switch da instância da classe TraceSource onde também definimos o listener TextWriterTraceListener para persistir os dados em um arquivo texto chamado “Log.txt”. Finalmente, invocamos o método TraceEvent para inserirmos uma nova entrada no tracing. O primeiro método, informamos como TraceEventType o valor Critical. Já na segunda vez que invocamos o mesmo método, informamos como TraceEventType o valor Information. Sendo assim, o segundo não será persistido, justamente porque o switch somente aceita, no mínimo, eventos do tipo Critical. Isso irá garantir que, desenvolvedores que utilizam o componente, não grave informações desnecessárias. Finalmente, se quisermos configurar o mesmo TraceSource, só que agora dentro do arquivo de configuração, podemos proceder da seguinte forma: App.Config <?xml version="1.0" encoding="utf-8" ?> <configuration> <system.diagnostics> <sources> <source name="Trace - Componente 1" switchName="switch"> <listeners> <add name="myLocalListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="Log.txt" /> </listeners> </source> </sources> <switches> <add name="switch" value="Information" /> </switches> </system.diagnostics> </configuration> E para utilizá-lo dentro da aplicação, faz: VB.NET Imports System.Diagnostics Dim source As New TraceSource("Trace - Componente 1")

Page 149: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 27

Israel Aece | http://www.projetando.net

27

source.TraceEvent(TraceEventType.Critical, 12, "Mensagem crítica") source.TraceEvent(TraceEventType.Information, 22, "Mensagem de informação") source.Close() C# using System.Diagnostics; TraceSource source = new TraceSource("Trace - Componente 1"); source.TraceEvent(TraceEventType.Critical, 12, "Mensagem crítica"); source.TraceEvent(TraceEventType.Information, 22, "Mensagem de informação"); source.Close(); WMI – Windows Management Instrumentation Windows Management Instrumentation (WMI) é parte do sistema operacional que fornece uma infraestrutura para controle, gerenciamento e informações para o gerenciamento do sistema operacional. Ele pode fornecer informações relevantes para monitoramento de aplicações e, conseqüentemente, reportar de forma amigável ao usuário. O .NET Framework fornece uma variedade de classes que nos auxiliam para resgatar informações referentes ao sistema operacional. Essas classes estão disponíveis dentro do namespace System.Management que, é necessário fazer a referência à System.Management.dll na aplicação se desejar utilizar o WMI. Através da imagem abaixo é possível visualizar a arquitetura do WMI dentro do mundo .NET:

Page 150: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 28

Israel Aece | http://www.projetando.net

28

Imagem 4.7 – Arquitetura WMI dentro do .NET Framework.

O namespace System.Management fornece classes para manipularmos o WMI de uma forma simples e bastante produtiva, possibilitando escrever as queries que utilizamos para extrairmos informações do sistema operacional, dispositivos e outras aplicação, de uma forma semelhante ao que existe atualmente com a linguagem SQL. Dentro deste namespace temos a classe chamada ManagementObjectSearcher, que é uma das mais conhecidas e utilizadas. Essa classe é comumente utilizada para recuperar diversos tipos de informações, como por exemplo enumerar todos os drives de um determinado computador, os adaptadores de rede, processos que está sendo executados e muitos outros objetos dentro do sistema. Quando desejamos recuperar esse tipo de informação, utilizamos uma query, baseada em SQL. Conseqüentemente, isso irá possibilitar colocarmos critérios na query para extrairmos informações mais precisas, como por exemplo, exibir somente os processos que estão atualmente pausados, etc.. Essa query é representada por um objeto do tipo ObjectQuery. A imagem abaixo ilustra a hierarquia das queries disponíveis dentro deste namespace:

Page 151: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 29

Israel Aece | http://www.projetando.net

29

Imagem 4.8 – Estrutura das queries disponíveis para WMI

Como a classe ManagementObjectSearcher permite buscar objetos baseados em uma query, ela fornece um método chamado Get que retorna uma objeto (coleção) do tipo ManagementObjectCollection, contendo todos os objetos encontrados, sendo cada um deles representados por um objeto do tipo ManagementObject. Esse objeto representa os dados de um determinado objeto WMI que foi encontrado pela query. Para exemplificar a utilização do que vimos até o momento, o código abaixo exibe como recuperar todos os processos que estão sendo executados na máquina: VB.NET Imports System.Management Dim searcher As New ManagementObjectSearcher("SELECT * FROM Win32_Service WHERE Started = TRUE") For Each service As ManagementObject In searcher.Get() Console.WriteLine("Serviço = " & service("Caption")) Next C# using System.Management; ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Service WHERE Started = TRUE"); foreach (ManagementObject service in searcher.Get())

Page 152: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 30

Israel Aece | http://www.projetando.net

30

Console.WriteLine("Serviço = " + service["Caption"]); Um detalhe no código acima é com relação ao objeto ManagementObject. Ele possui uma propriedade default (indexer em Visual C#) que expõe uma coleção do tipo PropertyDataCollection contendo todas as propriedades de um determinado objeto. O único problema aqui é que, por questões de genericidade, você deverá passar uma string contendo o nome da propriedade do objeto que deseja recuperar. Isso vai implicar em saber as propriedades que o objeto possui pois, se passar um nome que não existe, uma excessão do tipo ManagementException será atirada. Ainda dentro deste mesmo namespace temos a classe ManagementEventWatcher. Essa classe disponibiliza um evento chamdo EventArrived que é disparado quando um novo evento acontece. Esse evento, quando assinado, servem para notificar a aplicação que o utiliza para efetuar alguma ação customizada. Esse evento recebe em seu parâmetro um objeto do tipo EventArrivedEventArgs, contendo informações relacionadas ao evento que ocorreu. O código abaixo exibe como implementar essa classe para monitorar os eventos: VB.NET Imports System.Management Sub Main() Dim watcher As New ManagementEventWatcher( _ New WqlEventQuery( _ "SELECT * FROM __InstanceCreationEvent " & _ "WITHIN 1 WHERE TargetInstance ISA ""Win32_Process""")) AddHandler watcher.EventArrived, AddressOf EventArrived watcher.Start() System.Threading.Thread.Sleep(180000) watcher.Stop() End Sub Private Sub EventArrived(ByVal sender As Object, _ ByVal e As EventArrivedEventArgs) Console.WriteLine("Processo criado = " & _ DirectCast(e.NewEvent("TargetInstance"), ManagementBaseObject)("Caption")) End Sub C# using System.Management; static void Main(string[] args) {

Page 153: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 31

Israel Aece | http://www.projetando.net

31

ManagementEventWatcher watcher = new ManagementEventWatcher(new WqlEventQuery( @"SELECT * FROM __InstanceCreationEvent WITHIN 1 " + @"WHERE TargetInstance ISA ""Win32_Process""")); watcher.EventArrived += new EventArrivedEventHandler(EventArrived); watcher.Start(); System.Threading.Thread.Sleep(180000); watcher.Stop(); } static void EventArrived(object sender, EventArrivedEventArgs e) { Console.WriteLine("Processo criado = " + ((ManagementBaseObject)e.NewEvent["TargetInstance"])["Caption"]); } A partir do código acima, instanciamos a classe ManagementEventWatcher para monitorar os processos que estão sendo criados. Neste momento, temos uma nova classe chamada WqlEventQuery, que é uma classe que trabalha exclusivamente com eventos do WMI. A query informada no exemplo, monitora os processos que estão sendo criados. Em seguida, assinamos o evento EventArrived e, especificamos qual o procedimento que será disparado quando o evento ocorrer. Finalmente, chamamos o método Start do objeto ManagementEventWatcher para iniciar o monitoramento e, para fins de exemplo, suspendemos a execução da thread através do método Sleep, mantendo-a parada por 3 minutos. Nesse momento, todo e qualquer processo que for iniciado, como por exemplo, o bloco de notas, a calculadora do Windows, o evento EventArrived será disparado e, no caso acima, uma mensagem contendo o nome do processo recém iniciado, será escrita na tela. Ainda temos um outro namespace chamado System.Management.Instrumentation que disponibiliza um conjunto de classes para que as aplicações, também construídas em .NET, possam passar para o Object Manager informações a seu respeito, informações quais, mais tarde serão acessadas pelas aplicações de monitoramento, via classes disponíveis dentro do namespace System.Management. Basicamente para expor informações para o WMI para que aplicações de monitoramento consumam, é necessário criarmos uma classe que herde de uma classe abstrata chamada BaseEvent. Essa classe abstrata implementa a Interface IEvent, qual possui um método chamado Fire. Esse método delega a chamada para a classe Instrumentation que, efetivamente invocará o evento e, conseqüentemente, mandará para o WMI. Para já ilustrarmos, abaixo está somente a classe, que herda diretamente da classe BaseEvent e adiciona os propriedades necessárias que armazenarão as informações que devem ser expostas ao WMI e também as aplicações de monitoramento.

Page 154: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 32

Israel Aece | http://www.projetando.net

32

VB.NET Imports System.Management.Instrumentation Public Class EventoAppTeste Inherits BaseEvent Private _codigo As Integer Private _nome As String Public Sub New(ByVal codigo As Integer, ByVal nome As String) Me._codigo = codigo Me._nome = nome End Sub Public Property Codigo() As Integer Get Return Me._codigo End Get Set(ByVal value As Integer) Me._codigo = value End Set End Property Public Property Nome() As String Get Return Me._nome End Get Set(ByVal value As String) Me._nome = value End Set End Property End Class C# using System.Management.Instrumentation; public class EventoAppTeste : BaseEvent { private int _codigo; private string _nome; public EventoAppTeste(int codigo, string nome) { this._codigo = codigo; this._nome = nome; } public int Codigo {

Page 155: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 33

Israel Aece | http://www.projetando.net

33

get { return this._codigo; } set { this._codigo = value; } } public string Nome { get { return this._codigo; } set { this._codigo = value; } } } A seguir, é necessário incluirmos dentro do Assembly que terá esse evento, um instalador para que o mesmo seja capaz de inserir dentro do WMI o tal evento. Para a criação deste instador, iremos criar uma classe que herda diretamente da classe DefaultManagementProjectInstaller. Essa classe herda da classe Installer e possui a implementação necessária para a instalação de um Assembly que possui informações de instrumentação – WMI. Somente a herança é necessária e nenhum método precisa ser sobrescrito. O código abaixo mostra como criá-la: VB.NET Imports System.ComponentModel Imports System.Management.Instrumentation <RunInstaller(True)> Public Class Instalador Inherits DefaultManagementProjectInstaller End Class C# using System.ComponentModel; using System.Management.Instrumentation; [RunInstaller(true)] public class Instalador : DefaultManagementProjectInstaller { }

Page 156: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 34

Israel Aece | http://www.projetando.net

34

Nota: Para saber mais sobre instaladores e sobre o atributo RunInstallerAttribute, consulte o Capítulo 3. Ainda, neste mesmo Assembly é necessário adicionarmos um atributo chamado InstrumentedAttribute no arquivo AssemblyInfo. Esse atributo também está contido dentro do namespace System.Management.Instrumentation e indica que o Assembly possui informações de instrumentação. O trecho de código abaixo exibe a configuração deste atributo dentro do projeto onde o evento foi criado, informando em seu construtor o namespace que será utilizado pela instrumentação: VB.NET Imports System.Management.Instrumentation <Assembly: Instrumented("Root/Default")> C# using System.Management.Instrumentation; [assembly: Instrumented("Root/Default")] Finalmente, para adicionarmos o evento dentro do WMI, é necessário instalarmos o Assembly, mais precisamente o instalador que está contido dentro dele. Para isso, temos duas possibilidades para instalá-lo:

1. Empacotar em um arquivo MSI 2. Rodar o utilitário installutil.exe

Para fins de exemplo, iremos optar pela segunda opção: C:\>installutil C:\App.exe ... ... ... The transacted install has completed. Nota: Atente-se para abrir o Prompt do Visual Studio .NET ao invés do Prompt fornecido pelo Windows, para evitar escrever todo o caminho até o utilitário. Quando o utilitário installutil.exe encontrar o instalador dos eventos de WMI dentro do Assembly, ele fará todo o trabalho necessário para adicioná-lo dentro da arquitetura do WMI e possibilitará outras aplicações a consumí-los. Uma vez criado os eventos dentro do WMI, o mesmo Assembly que o cria, geralmente é o mesmo que o invoca (via método Fire) para notificar o WMI. Sendo assim, para fazermos um teste, temos que criar

Page 157: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 35

Israel Aece | http://www.projetando.net

35

instâncias da classe EventoAppTeste, definirmos os valores das propriedades e invocar o método Fire. O código a seguir mostra um exemplo dentro de um laço For de 10 iterações, suspendendo a thread por 5 segundos: VB.NET For i As Integer = 0 To 10 Dim ev As New EventoAppTeste(I, "Nome " + i.ToString()) ev.Fire() System.Threading.Thread.Sleep(5000); Next C# for (int i = 0; i < 10; i++) { new EventoAppTeste(i, "Nome " + i.ToString()).Fire(); System.Threading.Thread.Sleep(5000); } Esse laço fará com que ele mande 10 notificações do evento para o WMI e agora, compete a cada aplicação que quiser monitorar esses eventos, apanhá-los e exibir da forma que achar mais conveniente. No nosso caso, utilizaremos o mesmo exemplo que analisamos um pouco mais acima, quando utilizamos a classe ManagementEventWatcher, mudando ligeiramente a query para apenas recuperarmos os eventos do tipo EventoAppTeste. O códig abaixo mostra um exemplo na íntegra de um cliente que deseja recuperar um determinado tipo de evento: VB.NET Imports System.Management Sub Main() Dim watcher As New ManagementEventWatcher("root/default", _ "SELECT * FROM EventoAppTeste") AddHandler watcher.EventArrived, AddressOf EventArrived watcher.Start() System.Threading.Thread.Sleep(180000) watcher.Stop() End Sub Private Sub EventArrived(ByVal sender As Object, _ ByVal e As EventArrivedEventArgs) Console.WriteLine(e.NewEvent.Properties("Codigo").Value) Console.WriteLine(e.NewEvent.Properties("Nome").Value) End Sub

Page 158: Livro de .NET - Israel Aece

Capitulo 4 – Monitoramento e depuração de aplicações 36

Israel Aece | http://www.projetando.net

36

C# using System.Management; static void Main(string[] args) { ManagementEventWatcher watcher = new ManagementEventWatcher("root/default", "SELECT * FROM EventoAppTeste"); watcher.EventArrived += new EventArrivedEventHandler(EventArrived); watcher.Start(); System.Threading.Thread.Sleep(180000); watcher.Stop(); } static void EventArrived(object sender, EventArrivedEventArgs e) { Console.WriteLine(e.NewEvent.Properties["Codigo"].Value); Console.WriteLine(e.NewEvent.Properties["Nome"].Value); } A única mudança considerável neste código em relação ao que vimos um pouco mais acima, é com relação ao construtor da classe ManagementEventWatcher. No primeiro parâmetro, ele recebe uma string que identifica o escopo (namespace) que o watcher deverá monitorar; já o segundo parâmetro, é a query que vai definir qual evento deverá ser monitorado e, se repararem, a cláusula FROM recebe o nome do evento que criamos anteriormente, ou seja, EventoAppTeste.

Page 159: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 1

Israel Aece | http://www.projetando.net

1

Capítulo 5 Manipulando o sistema de arquivos Introdução A maioria das aplicações que existem atualmente sempre necessitam de, alguma forma, manipular arquivos que estão em algum local do disco. Sejam arquivos de configurações, texto, XML, etc.. Geralmente as aplicações também utilizam o sistema de arquivos para persistir alguns objetos para mais tarde por recuperá-los e, para isso, podemos utilizar o sistema de arquivos. Além disso, há uma série de instituições, mais precisamente os bancos, utilizam arquivos para a comunicação entre o banco e os clientes. Com isso, o cliente precisa tanto gerar o arquivo para quando quiser enviar algo ao banco como ter a possibilidade de ler e interpretar o arquivo quando o banco disponibilizar o retorno. A finalidade deste capítulo é mostrar como utilizar as classes contidas dentro do .NET Framework para a manipulação de arquivos. Inicialmente falaremos sobre como recuperar as informações a nível de drive, arquivo e diretórios. Vamos verificar como proceder manipular um caminho até um diretório ou arquivo. Para finalizar esta primeira parte, iremos por em funcionamento o objeto que a Microsoft disponibilizou para monitorar um determinado diretório. Já na segunda parte, vamos aprender como ler e salvar os dados a partir de streams. Além disso, veremos as alternativas, agora embutidas, que permitem comprimir e descomprimir um arquivo. Finalmente, vamos abordar o uso de um objeto que existe desde a primeira versão do .NET Framework: a StringBuilder. Essa classe é responsável por manipular uma string de forma bem eficiente, principalmente quando se diz respeito a concatenação de strings. Extraindo informações do sistema de arquivos a partir de classes fornecidas pelo .NET Framework O .NET Framework disponibiliza um namespace chamado System.IO que contém todas as classes necessárias para a manipulação de arquivos e diretórios. Algumas classes também fornecem suporte assíncrono, o que permite ler dados de um arquivo sem a necessidade de “congelar” a tela que o usuário está utilizando. Veremos a partir de agora três classes interessantes que são utilizadas para trabalhar com todas as espécies de objetos que temos a nível de sistema de arquivos: Drive, Diretório e Arquivo. Drivers A partir do .NET 2.0 temos uma classe chamada DriveInfo. Essa classe basicamente acessa informações de um determinado drive. Essa classe possui vários métodos e propriedades para manipular um determinado drive do computador. Além desses

Page 160: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 2

Israel Aece | http://www.projetando.net

2

membros de instância, temos um método estático chamado GetDrives que retorna um array de objetos do tipo DriveInfo onde, cada um, representa um drive do computador onde a aplicação está sendo executada. Entre as propriedades da classe DriveInfo, temos: Propriedade Descrição AvaliableFreeSpace Indica a quantidade de espaço disponível no disco. DriveFormat Especifica o sistema de arquivo, como NTFS ou FAT32 DriveType Indica o tipo de drive, pode ser:

• CDRom • Fixed • Unknow • Network • NoRootDirectory • Ram • Removable

IsReady Retorna um valor booleano indicando se o drive está pronto para ser acessado/lido.

Name Nome do drive. RootDirectory Retorna o diretório raiz do drive. TotalFreeSpace Indica o total de espaço disponível no drive. TotalSize Retorna o total de espaço que o drive possui. VolumeLabel Recupera o rótulo do volume do drive. Para exemplificar o uso desta classe, podemos fazer como é mostrado através do código abaixo: VB.NET Imports System.IO For Each d As DriveInfo In DriveInfo.GetDrivers() If d.IsReady Then Console.WriteLine(d.DriveType.ToString()) Console.WriteLine(d.Name) End If Next C# using System.IO; foreach (DriveInfo d in DriveInfo.GetDrives()) { if (d.IsReady) { Console.WriteLine(d.DriveType.ToString()); Console.WriteLine(d.Name);

Page 161: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 3

Israel Aece | http://www.projetando.net

3

} } Diretórios Assim como a classe DriveInfo, também temos uma classe estática chamada Directory que permite manipularmos os diretórios que existem na máquina em que a aplicação está sendo executada ou de algum outro local, dependendo da autorização. Esta classe expõe métodos estáticos para criar, mover, renomear e excluir diretórios. Além disso, permite recuperar todos os seus subdiretórios e arquivos para manipulá-los ou exibí-los. Ainda há a classe DirectoryInfo que também permite executar as mesmas operações que a classe Directory fornece, mas agora para um diretório específico. Se você precisar invocar várias propriedades e/ou métodos relacionados a um diretório, considere o uso desta classe em relação a classe Directory porque a checagem de segurança não será sempre necessária. Caso o teu cenário não seja esse, então opte por invocar os métodos estáticos da classe Directory. Entre as propriedades disponíveis pela classe DirectoryInfo, podemos citar as principais: Propriedade Descrição CreationTime Retorna a data de criação do diretório. FullName Retorna o caminho completo do diretório, desde a sua raiz. Name Retorna o nome do diretório. Parent Retorna o diretório que é “pai” do diretório corrente. Root Retorna o nível mais alto onde, na maioria dos casos, é o drive. Para citar alguns exemplos do uso desta classes, veremos abaixo como criar, ler o conteúdo e apagar um determinado diretório: VB.NET Imports System.IO Dim path As String = "c:\Temp\ArquivosProcessados" Dim dir As DirectoryInfo = Nothing If Not Directory.Exists(path) Then dir = Directory.CreateDirectory(path) End If If Not IsNothing(dir) Then ‘Sub-diretórios For Each subDir As DirectoryInfo in dir.GetDirectories() Console.WriteLine(subDir.Name) Next

Page 162: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 4

Israel Aece | http://www.projetando.net

4

‘Arquivos For Each fileName As FileInfo in dir.GetFiles() Console.WriteLine(filename.Name) Next dir.Delete(True) End If C# using System.IO; string path = @"c:\Temp\ArquivosProcessados"; DirectoryInfo dir = null; if (!Directory.Exists(path)) { dir = Directory.CreateDirectory(path); } if (dir != null) { //Sub-diretórios foreach (DirectoryInfo subDir in dir.GetDirectories()) Console.WriteLine(subDir.Name); //Arquivos foreach (FileInfo fileName in dir.GetFiles()) Console.WriteLine(fileName.Name); dir.Delete(true); } Como podemos ver, criamos o diretório a partir do método estático CreateDirectory da classe Directory. Depois de criado, recuperamos a instância do tipo DirectoryInfo que traz as informações a respeito do diretório recém criado. De posse desta instância, podemos ter acesso a diversas propriedades e métodos para manipular o diretório. Se analisarmos o código acima, utilizarmos dois métodos: GetDirectories e GetFiles. O primeiro deles retorna um array de DirectoryInfo contendo todos os sub-diretórios de um diretório qualquer. Já o segundo também retorna um array de FileInfo contendo todos os arquivos de um determinado diretório. Esse método ainda possui um overload que permite informar qual tipo de arquivos que desejamos recuperar., como por exemplo: se desejarmos apenas recuperar os arquivos texto, então poderíamos fazer: dir.GetFiles(“*.txt”). Como esses métodos são invocados a partir de uma instância do tipo DirectoryInfo, todas as informações são pertinentes ao diretório nele informado. Finalmente chamamos o método Delete para excluir o diretório. O parâmetro True que passamos para ele, indica que será uma exclusão recursiva, ou seja, tudo que estiver dentro dele, também será excluído.

Page 163: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 5

Israel Aece | http://www.projetando.net

5

Nota: Quando invocamos os métodos estáticos GetDirectories e GetFiles da classe Directory, é retornado um array de string contendo os nomes dos diretórios e arquivos, respectivamente. Arquivos De forma parecida aos anteriores, temos também duas classes para manipulação de arquivos: File e FileInfo. A classe File fornece métodos estáticos para as operações mais típicas com arquivos, como por exemplo, criação, cópia, exclusão e a movimentação de arquivos. Já a classe FileInfo traz informações completas de um determinado arquivo, além também, de fornecer as operações típicas e, como já era de se esperar, operando apenas com o arquivo que ela representa. Assim como a classe DirectoryInfo, utilize-a somente se precisar efetuar várias operações em cima deste arquivo. Do contrário, opte pela uso da classe File que, para uma única operação, é mais performática. Entre as propriedades disponíveis pela classe FileInfo, podemos citar as principais: Propriedade Descrição CreationTime Retorna a data de criação do arquivo. Directory Retorna uma instância da classe DirectoryInfo que representa o

diretório em que o arquivo está contido. DirectoryName Retorna o caminho completo até o diretório onde o arquifo está

contido. Extension Retorna a extensão do arquivo. Se o arquivo chamar “Arquivo.txt”

essa propriedade retornará “.txt”. FullName Resgata o caminho completo do arquivo. IsReadOnly Retorna um valor booleano indicando se o arquivo é ou não de

somente leitura. Length Retorna o tamanho do arquivo. Name Recupera o nome do arquivo. Para manipular os arquivos do disco utilizando essas classes, podemos analisar o código abaixo que explifica o que falamos acima com um trecho de código: VB.NET Imports System.IO Dim fileName As String = "Arquivo.txt" If Not File.Exists(fileName) Then File.Create(fileName) Dim info As New FileInfo(fileName) Console.WriteLine(info.CreationTime)

Page 164: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 6

Israel Aece | http://www.projetando.net

6

File.Delete(fileName) End If C# using System.IO; string fileName = "Arquivo.txt"; if (!File.Exists(fileName)) { File.Create(fileName); FileInfo info = new FileInfo(fileName); Console.WriteLine(info.CreationTime); File.Delete(fileName); } Manipulando caminhos (paths) Muitas vezes precisamos manipular os caminhos físicos que temos para atender uma determinada necessidade. Para isso, sempre manipulamos a string que contém o caminho completo, até chegarmos no valor que desejamos. Como isso é muito trabalhoso e pode gerar alguns erros, a Microsoft decidiu criar uma classe estática chamada Path. Essa classe encapsula todo o trabalho para manipular caminhos de arquivos e diretórios. Além disso, as operações são executadas para suportar multi-plataforma, ou seja, como isso pode variar de sistema operacional para sistema operacional, a classe Path se encarrega de retornar o valor correto baseando-se na plataforma que a aplicação está sendo executada. Para citar alguns métodos, vamos nos restringir aos mais populares e, para uma melhor descrição e exemplos, consulte a documentação do Visual Studio .NET ou, se desejar, pode consultar online: http://www.msdn.com/library. Método Descrição ChangeExtension Dado um arquivo com sua extensão e uma nova

extensão, esse método retorna uma nova string contendo o mesmo arquivo (com seu caminho), mas agora com a nova extensão informada.

Combine Permite combinar dois caminhos, fazendo todo o trabalho para manipulação dos separadores.

GetDirectoryName Dado um caminho, ele retorna uma string contendo apenas o caminho até o último diretório.

GetExtension Dado um nome de arquivo junto com a sua extensão, ele retorna apenas a extensão do arquivo. Exemplo: “Arquivo.txt” retorna “.txt”.

Page 165: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 7

Israel Aece | http://www.projetando.net

7

GetFileName Dado um caminho completo até um determinado arquivo, retorna apenas o nome do arquivo junto com a sua extensão.

GetFileNameWithoutExtension Dado um caminho completo até um determinado arquivo, retorna apenas o nome do arquivo, sem a extensão.

GetRandomFileName Retorna um nome de arquivo aleatório, podendo inclusive ser utilizado como nome para diretórios. Este método não cria o arquivo/pasta fisicamente.

GetTempFileName Cria fisicamente um arquivo vazio com um nome único e de extensão .TMP, que é retornado pelo método.

GetTempPath Retorna o caminho até o diretório de sistema que é a pasta temporária. Geralmente o caminho é: C:\Documents and Settings\<Usuario>\Local Settings\Temp\

HasExtension Retorna um valor booleano indicando se o caminho possui ou não uma extensão de arquivo.

Monitoramento de diretórios O namespace System.IO fornece uma classe um tanto quanto interessante. Ela chama-se FileSystemWatcher que tem a finalidade de monitorar um determinado diretório e, qualquer mudança que nele aconteça, a classe é capaz de notificar a aplicação e, conseqüentemente, você pode tratar isso da forma que desejar. Essa classe recebe em seu construtor o caminho que o watcher irá monitorar. Entre os eventos que esta classe fornece temos: Created, Changed, Deleted e Renamed. O primeiro deles é invocado pelo runtime quando um novo arquivo ou diretório é criado. O segundo é disparado quando um arquivo ou diretório é alterado; já o evento Deleted é disparado quando algum diretório ou arquivo é excluído e, finalmente, o evento Renamed, é disparado quando algum diretório ou arquivo é renomeado. Uma propriedade também muito importante é a propriedade EnableRaisingEvents. Essas propriedade recebe um valor booleano indicando se o watcher está ou não habilitado. Enquanto essa propriedade estiver definida como False, os eventos não serão disparados. Para exemplificar o uso desta classe, criaremos um wrapper para o mesmo que encapsulará todo o trabalho que o FileSystemWatcher irá desempenhar dentro da aplicação. Esse criação deste wrapper é mostrado abaixo: VB.NET Imports System.IO Public Class FileWatcher

Page 166: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 8

Israel Aece | http://www.projetando.net

8

Implements IDisposable Private _watcher As FileSystemWatcher Public Sub New(ByVal path As String) Me._watcher = New FileSystemWatcher(path) Me.InitializeEvents() Me._watcher.EnableRaisingEvents = True End Sub Private Sub InitializeEvents() AddHandler Me._watcher.Created, _ AddressOf Me._watcher_Created AddHandler Me._watcher.Changed, _ AddressOf Me._watcher_Changed AddHandler Me._watcher.Deleted, _ AddressOf Me._watcher_Deleted AddHandler Me._watcher.Renamed, _ AddressOf Me._watcher_Renamed End Sub Private Sub _watcher_Created(ByVal sender As Object, _ ByVal e As FileSystemEventArgs) Me.Show(e) End Sub Private Sub _watcher_Renamed(ByVal sender As Object, _ ByVal e As RenamedEventArgs) Me.Show(e) End Sub Private Sub _watcher_Deleted(ByVal sender As Object, _ ByVal e As FileSystemEventArgs) Me.Show(e) End Sub Private Sub _watcher_Changed(ByVal sender As Object, _ ByVal e As FileSystemEventArgs) Me.Show(e) End Sub Private Sub Show(ByVal e As FileSystemEventArgs) Console.WriteLine(e.ChangeType.ToString()) Console.WriteLine(e.FullPath) Console.WriteLine("---------------------") End Sub Public Sub Dispose() Implements IDisposable.Dispose

Page 167: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 9

Israel Aece | http://www.projetando.net

9

If Not IsNothing(Me._watcher) Then Me._watcher.Dispose() End If End Sub End Class C# using System.IO; public class FileWatcher : IDisposable { private FileSystemWatcher _watcher; public FileWatcher(string path) { this._watcher = new FileSystemWatcher(path); this.InitializeEvents(); this._watcher.EnableRaisingEvents = true; } private void InitializeEvents() { this._watcher.Created += new FileSystemEventHandler(_watcher_Created); this._watcher.Changed += new FileSystemEventHandler(_watcher_Changed); this._watcher.Deleted += new FileSystemEventHandler(_watcher_Deleted); this._watcher.Renamed += new RenamedEventHandler(_watcher_Renamed); } void _watcher_Created(object sender, FileSystemEventArgs e) { this.Show(e); } void _watcher_Renamed(object sender, RenamedEventArgs e) { this.Show(e); } void _watcher_Deleted(object sender, FileSystemEventArgs e) { this.Show(e); } void _watcher_Changed(object sender, FileSystemEventArgs e)

Page 168: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 10

Israel Aece | http://www.projetando.net

10

{ this.Show(e); } private void Show(FileSystemEventArgs e) { Console.WriteLine(e.ChangeType.ToString()); Console.WriteLine(e.FullPath); Console.WriteLine("---------------------"); } public void Dispose() { if (this._watcher != null) this._watcher.Dispose(); } } Como podemos analisar, assinamos todos os eventos e, quando uma determinada ação acontecer, os respectivos eventos serão disparados. Esse wrapper serve apenas para encapsular todo o trabalho que o FileSystemWatcher irá desenvolver na aplicação e que não seria obrigatoriamente necessário criá-lo para o FileSystemWatcher funcionar. Finalmente, para utilizar este wrapper, devemos fazer: VB.NET Using watcher As New FileWatcher("c:\Temp\") While (True) End While End Using C# using (FileWatcher w = new FileWatcher(@"c:\Temp\")) { while (true); } A classe é criada dentro de um bloco Using para garantir que o método Dispose seja executado mesmo que um erro ocorra. Para o exemplo, vamos criar, renomear, alterar e excluir um arquivo dentro do diretório c:\Temp. A imagem abaixo mostra os logs que foram efetuados a partir do wrapper que criamos acima:

Page 169: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 11

Israel Aece | http://www.projetando.net

11

Imagem 5.1 – Output do wrapper FileWatcher

Lendo e gravando dados em arquivos Quando estamos desenvolvendo uma aplicação que permite ler os dados de um repositório de dados, como por exemplo, o banco de dados, arquivos do disco ou qualquer outro recurso que seja capaz de persistir os dados, precisamos de alguma forma, extrair e interpretar essas informações. Um stream serve como condutor entre o código da aplicação e a fonte (arquivo, banco de dados, memória, etc.). O processo de mover os dados (bytes) entre a fonte de dados e a aplicação é chamada de streaming. O .NET Framework possui várias classes que permitem manipular os mais diversos tipos de streams. Essas classes estão contidas dentro do namespace System.IO. Dentro deste namespace temos a classe Stream. Essa é a classe base (e também abstrata) para todos os tipos de streams. Através de streams podemos realizar dois tipos de operações:

1. Leitura de dados trata-se de transferir os valores que estão na fonte para um tipo de dado estruturado, dentro da aplicação.

2. Escrita de dados em streams, que permite você enviar valores para serem gravados no stream.

A classe abstrata Stream fornece vários métodos interessantes, também abstratos, que devem ser obrigatoriamente implementados nas classes derivadas e, entre eles estão: Membro Descrição CanReader Indica se o stream suporta ou não leitura de dados. CanWriter Indica se o stream suporta ou não a escrita de dados. CanSeek Indica se o stream permite ou não manter e manipular um ponteiro da

posição dentro do stream.

Page 170: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 12

Israel Aece | http://www.projetando.net

12

Length Retorna o comprimento do stream em bytes. Position Indica a posição do ponteiro dentro do stream. Read Efetua a leitura de uma séria de bytes do stream. Write Grava uma séria de bytes dentro do stream. Seek Move o ponteiro do stream para um determinada posição. Flush Quando invocado, esse método salva todo o conteúdo do stream em sua

fonte. Close Fecha o stream. Quando esse método é chamado, todo oseu conteúdo é

salvo na fonte. Essa classe abstrata Stream é implementada em diversos outros tipos de streams e, entre eles, temos: FileStream, MemoryStream e BufferedStream. A primeira delas, FileStream, é um wrapper para um stream que manipula um determinado arquivo, já dando suporte a operações síncronas e assíncronas. Essa classe é utilizada para ler, salvar, abrir e fechar arquivos do disco, lembrando que ele armazena o conteúdo em buffer para ter um ganho de performance. A classe File (que vimos um pouco mais acima), fornece alguns métodos que, ao serem executados, retornam um stream do tipo FileStream. Entre esses métodos temos: Create, Open, OpenRead e OpenWrite. Para utilizar a classe FileStream, podemos fazer: VB.NET Imports System.IO Imports System.Text Dim conteudo As String = "Utilizando o FileStream via VB." Dim bytes() As Byte = Encoding.Default.GetBytes(conteudo) Using stream As FileStream = File.Create("Teste.txt") stream.Write(bytes, 0, bytes.Length) End Using C# using System.IO; using System.Text; string conteudo = "Utilizando o FileStream via C#."; byte[] bytes = Encoding.Default.GetBytes(conteudo); using (FileStream stream = File.Create("Teste.txt")) { stream.Write(bytes, 0, bytes.Length); }

Page 171: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 13

Israel Aece | http://www.projetando.net

13

Via método estático Create da classe File, criamos um arquivo chamado “Teste.txt” e atribuimos o retorno deste método, que será um objeto stream do tipo FileStream. Notem que isso está sendo feito dentro de um bloco using que, mesmo que algum erro aconteça entre esse bloco, o stream será fechado de forma segura, invocando o método Dispose automaticamente. Depois disso, utilizamos o método Write que escreve um conteúdo sincronamente no arquivo. Nota: A técnica do bloco using será utilizada daqui para frente. Dando seqüencia, vamos falar um pouco sobre a classe MemoryStream. Como o próprio nome diz, essa classe cria um stream que tem como repositório, a memória e, sendo assim, quando a aplicação é finalizada, esses valores serão perdidos, não tendo uma forma de persitência física. Apesar de “deficiência”, ela tem um ótimo benefício, que é justamente o armazenamento na memória, que possibilita um acesso bem mais rápido que o sistema de arquivo, FileStream. Como essa classe é “volátil”, o interessante é utilizá-la para manipular dados temporários, que não exigem serem persistidos. Ela também estende a classe Stream, fornecendo suporte a chamadas síncronas e assíncronas. A utilização dessa classe é bem semelhante ao que vimos um pouco acima com o FileStream. O trecho de código abaixo exemplifica o uso dela: VB.NET Imports System.IO Imports System.Text Dim conteudo As String = "Utilizando o FileStream via VB." Dim bytes() As Byte = Encoding.Default.GetBytes(conteudo) Dim length As Integer = bytes.Length Using ms As New MemoryStream(bytes) ms.Write(bytes, 0, length) ms.Seek(0, SeekOrigin.Begin) Dim output() As Byte = New Byte(length) {} ms.Read(output, 0, length) Console.WriteLine(Encoding.Default.GetString(output)) End Using C# using System.IO; using System.Text; string conteudo = "Utilizando o FileStream via C#."; byte[] bytes = Encoding.Default.GetBytes(conteudo); int length = bytes.Length;

Page 172: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 14

Israel Aece | http://www.projetando.net

14

using (MemoryStream ms = new MemoryStream(length)) { ms.Write(bytes, 0, length); ms.Seek(0, SeekOrigin.Begin); byte[] output = new byte[length]; ms.Read(output, 0, length); Console.WriteLine(Encoding.Default.GetString(output)); } Apesar de ser bem semelhante ao FileStream, há um detalhe bem interessante que precisamos analisar. Esse detalhe é com relação ao construtor da classe MemoryStream. Há um overload que permite passar um array de bytes. Isso evitaria de chamar o método Write e Seek para escrever o conteúdo e retornar a posição do ponteiro para o ínicio, pois o código interno ao construtor se encarregaria de realizar todas essas tarefas. Ainda descendendo da classe Stream, temos a classe BufferedStream. Buffer é um bloco de bytes que residem na memória, usado para efetuar o cache de dados para evitar, a todo momento, fazer chamadas ao sistema operacional e, conseqüentemente, melhorar a performance. O buffer pode ser usado para leitura ou para gravação, mas nunca simultâneamente. A classe BufferedStream possui uma layer de buffer e é também responsável por gerenciá-la. O exemplo abaixo exibe como criar um arquivo através do método Create da classe File e atribuir o seu retorno, um objeto do tipo FileStream, a um BufferedStream: VB.NET Imports System.IO Using stream As New BufferedStream(File.Create("Teste.txt")) ‘ realização das operações End Using C# using System.IO; using (BufferedStream stream = new BufferedStream(File.Create("Teste.txt"))) { // realização das operações } Além desses streams que vimos até o momento, ainda temos ainda dois streams que são exclusivos para trabalhar com caracteres de entrada em um encoding específico. O primeiro deles é o StreamReader. Ele geralmente é utilizado para ler linhas de informações de um arquivo texto qualquer. A utilização desta classe é bem simples, ou

Page 173: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 15

Israel Aece | http://www.projetando.net

15

seja, precisamos somente no construtor dela, passar o caminho do arquivo que desejamos ler e, em seguida, invocar o método ReadLine ou o método ReadToEnd. A diferença entre os dois é que o primeiro lê apenas uma linha por vez, o que necessitaria você colocá-lo dentro de um loop para ler todas as linhas do arquivo; já o segundo, em um único método, retorna todo o conteúdo do arquivo. O código abaixo ilustra o uso desta classe: VB.NET Imports System.IO Using stream As New StreamReader("c:\Temp\Teste.txt") Console.WriteLine(stream.ReadToEnd()) End Using C# using System.IO; using (StreamReader stream = new StreamReader ("c:\Temp\Teste.txt")) { Console.WriteLine(stream.ReadToEnd()); } Já a classe corresponde ao StreamReader para gravação é a classe StreamWriter. Trabalhamos basicamente da mesma forma: passamos para o construtor o caminho e nome do arquivo que desejamos salvar o conteúdo e, via método Write ou WrilteLine, vamos adicionando o conteúdo ao arquivo, conforme o exemplo abaixo: VB.NET Imports System.IO Using stream As New StreamWriter("c:\Temp\Teste.txt") stream.WriteLine("Gravando via StreamWriter no VB.") End Using C# using System.IO; using (StreamWriter stream = new StreamWriter ("c:\Temp\Teste.txt")) { stream.WriteLine("Gravando via StreamWriter no C#."); }

Page 174: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 16

Israel Aece | http://www.projetando.net

16

Finalmente, temos as classes BinaryReader e BinaryWriter. Essas duas classes servem para ler e gravar tipos de dados primitivos como valores binários em um encoding específico. A gravação de dados via BinaryWriter é tranquila, pois informamos o stream (ou o arquivo) em que os dados serão gravados e, em seguida, invocamos o método Write que é sobrecarregado, possuindo sobrecargas para os mais diversos tipos de dados que o .NET Framework suporta. A classe BinaryReader, que lê os dados de um determinado stream, precisa de alguns cuidados a parte. Primeiramente precisamos invocar o método PeekChar para que o BinaryReader, que retorna o próximo caracter disponível mas não avança a posição do ponteiro. A partir de agora, você chama os métodos respectivos a cada tipo que você salvo, exemplo: suponhamos que você criou um inteiro e uma string, logo, deve invocar os método ReadInt32 e ReadString, sucessivamente. Todos os métodos ReadXXX, além de retornar o valor correspondente, também é responsável por avançar a posição do ponteiro interno do stream. Para exemplificar o uso do que vimos aqui sobre o BinaryWriter e BinaryReader, veja o código abaixo: VB.NET Imports System.IO Using bw As New BinaryWriter(File.Create("Teste.bin")) bw.Write(123) bw.Write("VB.NET e C#") bw.Write(true) bw.Write(false) bw.Write(1.290) End Using Using br As New BinaryReader(File.Open("Teste.bin", FileMode.Open)) If Not br.PeekChar() = -1 Then Console.WriteLine(br.ReadInt32()) Console.WriteLine(br.ReadString()) Console.WriteLine(br.ReadBoolean()) Console.WriteLine(br.ReadBoolean()) Console.WriteLine(br.ReadDouble()) End If End Using C# using System.IO; using (BinaryWriter bw = new BinaryWriter(File.Create("Teste.bin"))) { bw.Write(123); bw.Write("VB.NET e C#");

Page 175: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 17

Israel Aece | http://www.projetando.net

17

bw.Write(true); bw.Write(false); bw.Write(1.290); } using (BinaryReader br = new BinaryReader(File.Open("Teste.bin", FileMode.Open))) { if (br.PeekChar() != -1) { Console.WriteLine(br.ReadInt32()); Console.WriteLine(br.ReadString()); Console.WriteLine(br.ReadBoolean()); Console.WriteLine(br.ReadBoolean()); Console.WriteLine(br.ReadDouble()); } } A imagem abaixo trata-se do arquivo “Teste.txt” com os valores salvos em formato binário:

Imagem 5.2 – Arquivo “Teste.bin” com seus valores binários

Compressão de dados Quando precisávamos comprimir ou descomprimir arquivos (streams) nas versões 1.x do .NET Framework tínhamos que recorrer a bibliotecas de terceiros para isso. A Microsoft se preocupou com essa necessidade e decidiu incorporar essa funcionalidade dentro do .NET Framework 2.0. É com isso que surge um novo namespace chamado System.IO.Compression, fornecendo classes que efetuam compressão e descompressão de streams. Atualmente temos duas classes contidas dentro deste namespace (que herdam diretamente da classe Stream): DeflateStream e GZipStream. A classe DeflateStream fornece métodos e propriedades para compressão e descompressão de streams utilizando o algoritmo Deflate. Essa classe não permite a compressão de arquivos com um tamanho maior que 4GB.

Page 176: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 18

Israel Aece | http://www.projetando.net

18

Já a classe GZipStream também utiliza o mesmo algoritmo da classe DeflateStream, mas pode ser estendido para utilizar outros formatos de compressão. Basicamente, a classe GZipStream é um wrapper para um DeflateStream, incluindo header, body e footer onde, a seção header contém informações de metadados a respeito do conteúdo comprimido, o body é a seção onde o conteúdo comprimido é armazenado e a seção footer permite incluir valores de verificações de redundância cíclica para detectar o corrompimento dos dados. Além disso, essa classe ainda possibilita detectar erros durante a compressão e, se desejar compartilhar os dados comprimidos com outros programas, utilize o GZipStream ao invés da classe DeflateStream. Essa classe não permite a descompressão de arquivos com um tamanho maior que 4GB. Exemplos: Como o código para exemplificar o uso destas classes é um pouco extenso, ele não será colocado aqui, mas você pode analisar a utilização deles na aplicação de demonstração. Manipulação de Strings Utilização da classe StringBuilder Quando manipulamos strings dentro de uma aplicação .NET elas são mapeadas para a classe System.String. Essas strings são imutáveis, ou seja, quando fazemos uma operação onde alteramos o seu valor ou até mesmo chamando um método dela, como por exemplo, o método Substring, uma nova string é gerada. Outro ponto importante é quando estamos falando de concatenação de strings. Geralmente utilizamos isso quando precisamos montar um valor proveniente de uma base de dados, ou até mesmo de uma construção mais complexa. Popularmos essas concatenações são realizadas dentro de loops que, se não tomarmos um pouco de cuidado, a performance pode ser comprometida. A questão da performance também é bastante importante quando precisamos manipular a string, como por exemplo, inserindo um valor em um local especificado, remover uma string ou até mesmo trocar uma string por outra dentro de uma string maior. Tudo isso que as vezes pode ser muito custoso, a Microsoft se antecipou e criou uma classe para manipular de forma mais ágil as strings. Trata-se da classe StringBuilder, que está contida dentro do namespace System.Text. Esse classe representa agora um string mutável, ou seja, ele pode sofrer inserções de novas strings, removê-las e trocar caracteres. Essa classe fornece uma porção de métodos que permitem manipular a string que está contida dentro da mesma. Entre os principais métodos e propriedades temos: Membro Descrição Capacity Define a quantidade máxima de memória alocada pela instância

corrente. A capacidade pode ser diminuida, mas não pode ser menor que o valor especificado pela propriedade Length e também não pode ser maior que o valor especificado pela propriedade MaxCapacity.

Page 177: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 19

Israel Aece | http://www.projetando.net

19

O valor padrão para essa propriedade é 16. Ao acrescentar caracteres dentro dele, o StringBuilder verifica se o array interno que armazena os caracteres está tentando aumentar além da sua capacidade. Se sim, automaticamente dobrará o tamanho dessa capacidade, alocando um novo array e copiando os caracteres anteriores para este novo e, conseqüentemente, o array original será descartado. Esse aumento dinâmico prejudica a performance e, sendo assim, tente definir uma capacidade para evitar isso.

Chars Baseando-se em um índice, retorna um tipo Char que indica o caracter que está dentro da string.

Length Retorna um número inteiro que indica o tamanho da string. MaxCapacity Define a quantidade máxima de caracteres que a instância pode

armazenar. Append Esse é um método que possui vários overloads, basicamente

suportando em cada overload um tipo de dado diferente. Quando esse método é invocado, o valor passado para o mesmo será adicionado ao fim da string.

AppendFormat Permite adicionar um valor no final da string, mas a diferença em relação ao método Append é que permite formatar o valor antes dele ser efetivamente adicionado.

Insert Permite adicionar um valor em uma posição específica. Assim como o método Append, ele possui vários overloads que permitem passamos os mais diversos tipos para ser adicionado.

Remove Este método aceita dois números inteiros como parâmetro. O primeiro deles é a posição inicial dentro da string e, o segundo, a quantidade de caracteres que deseja remover da string.

Replace Modifica todas as ocorrências que forem encontradas dentro da string por um outro valor.

ToString Esse método permite converter a instância corrente da classe StringBuilder em uma string.

Para exemplificar o uso desta classe e de seus membros, o código abaixo manipula uma string que está contida dentro da classe StringBuilder: VB.NET Imports System.Text Dim sb As New StringBuilder(16, 150) Dim versoesNET As Object() = {"1.0", "1.1", "2.0"} sb.Append("Utilizando a Classe String Builder.") sb.AppendFormat(" Ela é disponilizada nas seguintes versões: {0}, {1} e {2}.", versoesNET) sb.Insert(34, " do namespace System.Text")

Page 178: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 20

Israel Aece | http://www.projetando.net

20

sb.Remove(0, 13) Console.WriteLine(sb.ToString()) C# using System.Text; StringBuilder sb = new StringBuilder(16, 150); object[] versoesNET = { "1.0", "1.1", "2.0" }; sb.Append("Utilizando a Classe String Builder."); sb.AppendFormat(" Ela é disponilizada nas seguintes versões: {0}, {1} e {2}.", versoesNET); sb.Insert(34, " do namespace System.Text"); sb.Remove(0, 13); Console.WriteLine(sb.ToString()); Inicialmente criamos um objeto do tipo StringBuilder, definindo a sua capacidade de memória alocada como 16 e a capacidade máxima de caracteres que ele pode suportar como 150. Em seguida, utilizamos os métodos que vimos detalhadamente na tabela, um pouco mais acima, suas respectivas funcionalidades. O resultado para ambos os códigos é: Classe String Builder do namespace System.Text. Ela é disponilizada nas seguintes versões: 1.0, 1.1 e 2.0. Considerações de Performance Assim como há o método Append para a classe StringBuilder, existe o método Concat para a classe String. A finalidade de ambos é adicionar um novo conteúdo ao fim de uma determinada string. A principal diferença entre eles é que o concatenação via classe String sempre retorna uma nova string pois, como vimos acima, as strings são imutáveis. Já a classe StringBuilder alterar o seu conteúdo interno de forma mais performática mas, temos um overhead a mais que é justamente a criação do objeto StringBuilder. Sendo assim, quantas vezes precisamos chamar o método Append para compensar a construção e o uso do objeto StringBuilder? Pois bem, existem uma porção de informações a respeito e abaixo tentaremos elencar os mais típicos, lembrando que isso pode variar de um cenário ao outro.

• Se você não tiver idéia do tamanho final da string, então utilize o StringBuilder se tiver ao menos sete concatenações

Page 179: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 21

Israel Aece | http://www.projetando.net

21

• Se você puder antecipar o tamanho da string (em pelo menos 30%), então utilize o StringBuilder se tiver ao menos cinco concatenações

• Se você puder antecipar precisamente o resultado da string, então utilize o StringBuilder se você tiver ao menos três concatenações

• Sem qualquer uma das condições atendidas, StringBuilder é mais rápido se você tiver ao menos três concatenações

Regular Expressions Expressões regulares forencem uma forma potente, eficiente e flexível para prcessar textos. Trata-se uma linguagem que foi criada e otimizada para manipular textos e, através delas, você pode facilmente extrair, ler, editar, trocar, excluir substrings de um determinado texto. Elas são ferramentas indispensáveis quando utilizamos arquivos de logs, processamento de arquivos HTML, validações de informações que o usuário fornece para a aplicação, entre outras inúmeras necessidades. As expressões regulares consistem em dois tipos de caracteres: literal (que são os caracteres normais) e os metacaracteres (símbolos). O conjunto desses caracteres dão as expressões regulares todo o poder de precessamento do texto. Para mostrar um exemplo de expressão regular, podemos citar: “\s2000”. Quando ela é aplicada em um texto, ela irá procurar por todas as ocorrências com o valor 2000 que estão precedidos de qualquer caracter de espaço, podendo ser um espaço simples ou um TAB. Como manipular expressões regulares não é uma tarefa fácil, o .NET Framework fornece uma gama de classes que permitem manipular as expressões regulares de forma fácil. Todas as classes estão contidas dentro de um namespace chamado System.Text.RegularExpressions e, entre as classes disponíveis temos: RegEx, Match , MatchCollection, Group, GroupCollection, Capture e CaptureCollection. Ainda temos um delegate chamado MatchEvaluator que representa um método que será invocado quando um determinado valor é encontrado dentro de um texto durante o processo de replace. Mas antes de analisar as classes que o .NET Framework fornece, precisamos entender um pouco mais sobre as expressões regulares e como construí-las. Vale lembrar que não iremos nos aprofundar muito porque esse assunto é muito mais extenso do que veremos aqui. Através da tabela abaixo, vamos analisar, inicialmente, os metacaracteres e o que eles representam dentro de uma expressão regular: Metacaracter Descrição Grupo . Estipula qualquer caracter.

Exemplo:

1. “n.o”: A expressão irá procurar por essa palavra, não importando que caracter esteja substituindo o ponto.

Representantes

Page 180: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 22

Israel Aece | http://www.projetando.net

22

[ ] Defina uma lista de caracteres permitidos. Permite também definir um intervalo de caracteres. Exemplo:

2. “n[ãa]o”: Se executando a expressão com o ponto para procurarmos, por exemplo, pela palavra “não”, se existir palavras no texto que atendam aos caracteres n e o, eles também serão retornados, exemplo: “teclando”, “expandindo”. Como a idéia é recuperar apenas a palavra “não”, independente de ter colocado o til ou não, pode optar por essa expressão regular.

3. “[0-9]”: Permite somente números que estiverem entre 0 e 9.

Representantes

[^] Defina uma lista de caracteres proibidos. Exemplo:

4. “[^0-9]”: A partir desta expressão regular, você está querendo procurar por todos os caracteres, com exceção dos números de 0 a 9.

5. “[^a-f]”: Neste, você está procurando por todos os caracteres, com exceção do intervalo de “a” até “f” (minúsculo).

Representantes

? Zero ou um (opcional). Exemplo:

6. “bater?”: Essa expressão indica que o “r” será opcional e, sendo assim as palavras “bate” e “bater” são perfeitamente válidas.

Quantificadores

* Zero, um ou mais. Exemplo:

7. “10*”: O asterísco não se importa com o valor, ou seja, pode ter, não ter ou pode ter inúmeras ocorrências. No caso mostrado no exemplo, todos os valores que contemplem ou não o zero será retornado, tipo: 1, 100, 1000, 10000, etc. Mas o 2 não se enquadrara.

Quantificadores

+ Um ou mais. Exemplo:

8. “10+”: Neste caso, o zero precisa ocorrer pelo menos uma vez. Sendo assim, 10, 100,

Quantificadores

Page 181: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 23

Israel Aece | http://www.projetando.net

23

1000, etc. seria retornado mas, 1 não se enquadra.

{n,m} De x até m. Exemplo:

9. “[a-g]{2,4}”: Permite encontrar dentro de uma string os caracteres de “a” até “g” que contenham no mínimo dois e no máxima quatro ocorrências.

Quantificadores

^ Início da linha. Exemplo:

10. “^[a-z]”: Neste cenário, a finalidade do acento circunflexo é apenas de indicar o ínicio da linha e, nesta expressão regular, estamos procurando por linhas que começam com caracteres de “a” até “z”.

Âncoras

$ Fim da linha. Exemplo:

11. “^$”: Essa expressão regular contempla uma linha vazia.

Âncoras

\b Ínicio ou fim de uma palavra. Exemplo:

12. “\ba”: Irá retornar todas as palavras que começa com a letra “a”.

13. “a\b”: Irá retornar todas as palavras que acaba com a letra “a”.

Âncoras

\ Define um caracter como literal. Exemplo:

14. “[0-9]\.[0-9]”: Neste caso, o metacaracter não exercerá a sua funcionalidade dentro do mundo das expressões regulares, sendo agora, um caracter normal.

-

| Ou um ou outro. Exemplo:

15. “item1|item2”: Permite encontrar um ou outro item especificado na criação.

-

( ) Delimita um grupo. Exemplo:

16. “([0-9]){2}”: Todo o conteúdo definido

-

Page 182: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 24

Israel Aece | http://www.projetando.net

24

dentro de um grupo são encarados como sendo um único bloco na expressão e, no nosso exemplo, ele trará todos os números que estiverem separados de dois em dois, ou seja, dado uma string do tipo “1234 567 89023”, os valores retornados são: 12, 34, 56, 89 e 02.

\1...\9 Retrovisores. Exemplo:

17. “([a-z]+)\1”: Os retrovisores permitem recuperarmos informações repetidas. Eles são utilizados em conjunto com os grupos e repetem o resultado (não a expressão) do grupo, ou seja, para o exemplo acima, o grupo procura por todas as letras entre “a” e “z” e, em conjunto com o “\1”, retornará o apenas os casos onde a letra seguinte é a mesma definida pelo grupo. Para o exemplo “ahjkko ppoijj yytrwwul ppmnbbvf” ele retornará: kk, pp, jj, yy, ww, pp, bb.

-

Ainda existem vários outros caracteres que utilizamos em expressões regulares, mas não vamos abordar todos aqui justamente por não ser o foco do capítulo. A seguir, veremos as classes que o namespace System.Text.RegularExpressions fornece para manipular strings e regular expressions. Assim como os caracteres, não analisaremos as classes fornecidas pelo namespace, pois como já sabemos, não é foco do capítulo e, vamos analisar somente as principais classes necessárias para tratarmos expressões regulares. Para iniciarmos, vamos analisar a classe RegEx. Essa classe representa uma expressão regular imutável, ou seja, de somente leitura, fornecendo métodos estáticos que permitem a análise de expressões regulares sem a necessidade de criar a instância da classe. O exemplo abaixo mostra como devemos proceder para utilizá-la. A expressão regular a seguir, verifica se o valor informado é ou não um número: VB.NET Imports System.Text.RegularExpressions Dim numero1 As String = "123a" Dim numero2 As String = "456" Console.WriteLine(Regex.IsMatch(numero1, "^[0-9]+$")) Console.WriteLine(Regex.IsMatch(numero2, "^[0-9]+$")) ‘Output False

Page 183: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 25

Israel Aece | http://www.projetando.net

25

True C# using System.Text.RegularExpressions; string numero1 = "123a"; string numero2 = "456"; Console.WriteLine(Regex.IsMatch(numero1, "^[0-9]+$")); Console.WriteLine(Regex.IsMatch(numero2, "^[0-9]+$")); //Output False True Neste momento apresentaremos uma nova classe chamada Match. Essa representa o resultado de uma análise de expressão regular que é retornado através do método Match da classe RegEx. O exemplo abaixo ilustra o uso desta classe: VB.NET Imports System.Text.RegularExpressions Dim numero1 As String = "123" Dim r As New Regex("^[0-9]+$") Dim match As Match = r.Match(numero1) Console.WriteLine(match.Success) Console.WriteLine(match.Value) Console.WriteLine(match.Length) ‘Output True 123 3 C# using System.Text.RegularExpressions; string numero1 = "123"; Regex r = new Regex("^[0-9]+$"); Match match = r.Match(numero1); Console.WriteLine(match.Success); Console.WriteLine(match.Value); Console.WriteLine(match.Length); //Output True

Page 184: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 26

Israel Aece | http://www.projetando.net

26

123 3 Mudamos ligeiramente o código para análise da expressão regular. Ao invés de chamarmos o método estático Match, optamos por criar a instância da classe RegEx, passando em seu construtor a expressão que será aplicada a string. Criamos um objeto do tipo Match e atribuimos a ele o retorno do método Match da classe RegEx. A classe Match possui algumas informações importantes a respeito do resultado da análise. Abaixo estão listados as principais propriedades: Propriedade Descrição Index Retorna um número inteiro indicando a posição onde o primeiro

caracter foi encontrado. Length Retorna um número inteiro indicando a posição inicial da substring. Success Através de um valor booleano, indica se foi a expressão foi ou não

satisfeita. Value Retorna a substring que foi encontrado a partir da expressão regular. Para encerrar a seção sobre expressões regulares e as classes do .NET Framework que as manipulam, vamos analisar a classe MatchCollection que, como o próprio nome diz, trata-se de uma coleção de objetos do tipo Match. Nos exemplos que vimos na tabela de metacaracteres há a possibilidade de retornar vários resultados. Sendo assim, utilizaremos um novo método da classe RegEx que é chamado de Matches. Lembre-se de que também existe esse mesmo método em sua forma estática, que não necessita da instância da classe e que voltaremos a utilizá-lo no exemplo a seguir: VB.NET Imports System.Text.RegularExpressions Dim coll As MatchCollection = _ Regex.Matches("1234 567 89023 2", "([0-9]){3}") For Each m As Match In coll Console.WriteLine(m.Value) Next C# using System.Text.RegularExpressions; MatchCollection coll = Regex.Matches("1234 567 89023 2", "([0-9]){3}"); foreach (Match m in coll) Console.WriteLine(m.Value);

Page 185: Livro de .NET - Israel Aece

Capítulo 5 – Manipulando o sistema de arquivos 27

Israel Aece | http://www.projetando.net

27

Para ambos os códigos, o resultado é o seguinte: 123 567 890

Page 186: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 1

Israel Aece | http://www.projetando.net

1

Capítulo 6 Serialização Introdução Para enviar objetos de um local para outro, é preciso que você converta o objeto em um determinado formato. Quando estamos desenvolvendo uma aplicação, é muito comum precisarmos transferir dados desta aplicação para uma outra qualquer. Neste momento precisamos fazer uso de um processo, chamado de serialização onde o objeto que deverá ser transferido será persistido em um determinado formato e, em seguida, será enviado para o destino. Quando o destino recebe a informação, antes de a processar, ele efetua o processo de deserialização onde, converteremos esse “pacote” em um objeto conhecido. Esse processo chama-se deserialização. O que é Serialização? Serialização (serialization) é o processo onde você converte um objeto em um stream de bytes para persistí-lo na memória, em um banco de dados ou em arquivo. A idéia é salvar o estado do objeto para que a aplicação seja capaz de restaurar o estado atual do objeto quando necessário. O processo inverso é chamado de deserialização (deserialization). O .NET Framework 2.0 já fornece classes e Interfaces para persistir os objetos em formato binário e SOAP e, além disso, traz também classes e Interfaces para que possa persistir o objeto em formato XML, para facilitar a comunicação entre diferentes plataformas e ainda, fornece mecanismo para podermos customizarmos a serialização do objeto, mudando o seu comportamente padrão para um mais específico. O formato da serialização vai depender para onde deseja enviar o objeto. Por exemplo, se desejarmos passar o objeto entre aplicações .NET, ou melhor, entre AppDomains diferentes, podemos serializar o objeto em formato binário. Se desejarmos enviar o objeto para um Web Service, então é necessário serializar este objeto em formato XML. A imagem abaixo ilustra como funciona o processo de serialização:

Imagem 6.1 – Processo de serialização

Page 187: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 2

Israel Aece | http://www.projetando.net

2

Quando você desenvolve uma aplicação que internamente faz o uso de objetos, quando a aplicação é fechada, todos os objetos são descartados juntamente com a mesma, ou seja, se você fez várias configurações em um determinado objeto, definiu valores em propriedades, invocou métodos que efetuaramos operações internas ao objeto, tudo isso será perdido quando a aplicação for finalizada. Se desejar manter esses valores, para mais tarde quando retornar a aplicação, é necessário serializar esse objeto fisicamente no disco. Para efetuar essa serialização podemos, em primeiro momento, apenas utilizar as classes padrões fornecidas pelo .NET Framework 2.0. Inicialmente, precisamos de um formatter. Como já vimos acima, a serialização pode ser realizada em um stream usando SOAP, XML ou Binary. Os formatters são utilizados para determinar em qual desses formatos o objeto será serializado. O .NET Framework fornece dois formatters: BinaryFormatter e SoapFormatter, onde ambas fornecem os métodos de serialização e deserialização. A classe BinaryFormatter está disponível dentro do namespace System.Runtime.Serialization.Formatters.Binary. Já a classe SoapFormatter encontra-se dentro do namespace System.Runtime.Serialization.Formatters.Soap só que é necessário adicionar referência à System.Runtime.Serialization.Formatters.Soap.dll. A classe BinaryFormatter, como o próprio nome diz, persite os dados em formato binário, serializando todos os membros da classe, inclusive os membros privados. Já a classe SoapFormatter persiste os dados em formato SOAP, que é uma XML especializado para facilitar o transporte de dados via web e permite apenas a serialização de membros definidos com o atributo Serializable. Ambas as classes fornecem também um método para serialização chamado Serialize e outro para deserialização, chamado de Deserialize, métodos provenientes da Interface IFormatter. O método Serialize recebe um stream que é onde o objeto, que é passado como segundo parâmetro, será persistido. Já o método Deserialize, dado um stream contendo o valor persitido, retorna um Object, resultando do processo de deserialização. Para exemplificar a utilização dos dois tipos de formatters que vimos até agora, vamos criar uma classe chamada Funcionario que terá duas propriedades, a saber: Nome e Salario, sendo a primeira do tipo string e a segunda do tipo double. A única diferença aqui é que temos que dizer ao runtime que a classe Funcionario é uma classe serializável. Como vimos anteriormente, precisamos aplicar a classe o atributo Serializable. Por questões de espaço, somente colocaremos aqui a declaração da classe para exibir como aplicar o atributo. A parte importante resume-se na criação de dois métodos auxiliares, sendo uma para serializar e outro para deserializar o objeto Funcionario. Vejamos o código a seguir: VB.NET Imports System.Runtime.Serialization.Formatters.Soap <Serializable()> Public Class Funcionario ‘... End Class

Page 188: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 3

Israel Aece | http://www.projetando.net

3

Private fileName As String = "Cliente.bin" Sub Main() Dim f As New Funcionario("João Mendes", 1300.0) Serialize(f) Dim f1 As Funcionario = Deserialize() Console.WriteLine(f1.Nome & " - " & f1.Salario) End Sub Private Sub Serialize(ByVal f As Funcionario) Using stream As Stream = File.Open(fileName, FileMode.Create) Dim formatter As New BinaryFormatter() formatter.Serialize(stream, f) End Using End Sub Private Function Deserialize() As Funcionario Dim f As Funcionario = Nothing Using stream As Stream = File.Open(fileName, FileMode.Open) Dim formatter As New BinaryFormatter() f = TryCast(formatter.Deserialize(stream), Funcionario) End Using Return f End Function C# using System.Runtime.Serialization.Formatters.Binary; [Serializable]public class Funcionario { //... } private static string fileName = "Cliente.bin"; static void Main(string[] args) { Funcionario f = new Funcionario("João Mendes", 1300.0); Serialize(f); Funcionario f1 = Deserialize(); Console.WriteLine(f1.Nome + " - " + f1.Salario); } private static void Serialize(Funcionario f) { using (Stream stream = File.Open(fileName, FileMode.Create)) {

Page 189: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 4

Israel Aece | http://www.projetando.net

4

BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, f); } } private static Funcionario Deserialize() { Funcionario f = null; using (Stream stream = File.Open(fileName, FileMode.Open)) { BinaryFormatter formatter = new BinaryFormatter(); f = formatter.Deserialize(stream) as Funcionario; } return f; } O exemplo acima faz a serialização e deserialização utilizando o formatter BinaryFormatter. Criamos dois métodos auxilares para facilitar o trabalho. O primeiro deles, Serialize, recebe a instância de um objeto Funcionario que o persiste. Já o segundo método auxiliar, Deserialize, retorna um objeto Funcionario que é extraído do stream, que foi anteriormente salvo. A imagem abaixo exibe o conteúdo do arquivo que serializamos:

Imagem 6.2 – Arquivo com o objeto serializado

Agora, se desejarmos persistir o mesmo objeto em formato SOAP, para facilitar o transporte dos dados via web, o código que vimos acima, muda ligeiramente. As únicas alterações é com relação ao formatter e o namespace onde ele reside. Temos então que trocar de BinaryFormatter para SoapFormatter. VB.NET Imports System.Runtime.Serialization.Formatters.Soap ‘... Dim formatter As New SoapFormatter() ‘...

Page 190: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 5

Israel Aece | http://www.projetando.net

5

C# using System.Runtime.Serialization.Formatters.Soap; //... SoapFormatter formatter = new SoapFormatter(); //... Finalmente temos o conteúdo salva em um formato XML:

Imagem 6.3 – Objeto persistido através da classe SoapFormatter

Nota: Assim como existe o atributo Serializable para indicarmos o que pode e o que não pode ser serializado no objeto, temos também o atributo NonSerializable que indica se um determinado membro pode ou não ser serializado. Serialização em formato XML Haverá alguns momentos onde será necessário persistir um determinado objeto em um formato para que ele seja independente de plataforma, ou seja, nem sempre você pode garantir que o cliente que irá consumir o objeto serializado utilize também .NET. Felizmente a Microsoft pensou nessa possibilidade e disponibilizou uma classe chamada XmlSerializer disponível dentro do namespace System.Xml.Serialization. Essa classe permite serializar propriedades públicas e membros do objeto em um formato XML para armazenamento ou mesmo para transporte.

Page 191: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 6

Israel Aece | http://www.projetando.net

6

Mais uma vez, se utilizarmos o mesmo exemplo que usamos para exemplificar a utilização das classes BinaryFormatter e SoapFormatter, basta alterarmos o formatter para XmlSerializer e, em seu construtor, especificar o tipo qual ele irá trabalhar. O trecho de código abaixo exibe como devemos configurar o XmlSerializer para serializarmos o objeto Funcionario que utilizamos um pouco mais acima em outros exemplos: VB.NET Imports System.Xml.Serialization ‘... Dim formatter As New XmlSerializer(GetType(Funcionario)) ‘... C# using System.Xml.Serialization; //... XmlSerializer formatter = new XmlSerializer(typeof(Funcionario)); //... A imagem abaixo exibe o output gerado pela serialização através da classe XmlSerializer:

Imagem 6.4 – Output da classe XmlSerializer

Como vimos anteriormente, a classe SoapFormatter serializa e deserializa objetos em formato SOAP, de acordo com as especificações estipuladas pelo W3C, incluindo

Page 192: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 7

Israel Aece | http://www.projetando.net

7

informações extras, exclusivas para o contexto, como por exemplo, o header de uma mensagem SOAP e é bem limitado em relação a customização do formato a ser gerado. Já a classe XmlSerializer faz o trabalho de persistência em formato XML, permitindo controlar como será o formato do documento XML que será gerado, ao contrário da classe SoapFormatter. Essa customização é possível graças aos atributos que estão disponíveis dentro do namespace System.Xml.Serialization, que podem ser aplicados nas propriedades e classes que desejamos persistir. Por padrão, a serialização XML não inclui informações sobre os tipos do objeto, assim como podemos reparar através da imagem 6.4. Isso acontece porque, durante o processo de deserialização, o tipo é extraído do próprio objeto. Por padrão, a classe XmlSerializer mapeia cada campo e propriedade do objeto para um elemento dentro do XML com o mesmo nome. Esses atributos estão divididos entre duas categorias: atributos para XML e atributos para SOAP e todos eles estão contidos dentro do namespace System.Xml.Serialization. Esses atributos controlam a forma que o XML e o SOAP é gerado, permitindo uma formatação mais customizada ao nosso cenário. As duas tabelas a seguir detalham cada um desses atributos: Atributo - SOAP Aplica-se Descrição SoapAttribute Campo público, propriedade,

parâmetro ou valor de retorno

O membro da classe será serializado como um atributo XML.

SoapElement Campo público, propriedade, parâmetro ou valor de retorno

A classe será serializada como um elemento XML.

SoapEnum Campo público que é um enumerador

Aplicado a enumeradores para customizar como seus valores serão serializados.

SoapIgnore Campos e propriedades públicos

A propriedade ou o campo será ignorado quando a classe for serializada.

SoapInclude Classes derivadas e métodos públicos para documentos WSDL

Um determinado tipo deve ser incluído quando for gerar schemas e, conseqüentemente, ser reconhecido quando for serializado.

SoapType Classes públicas A classe deve ser serializada como um tipo XML.

Atributo – XML Aplica-se Descrição XmlAnyAttribute Campo público, propriedade,

parâmetro ou valor de retorno que retorna um array de objetos do tipo XmlAttribute

Quando deserializado, o array será carregado com objetos do tipo XmlAttribute que representam todos os atributos XML desconhecidos do schema.

Page 193: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 8

Israel Aece | http://www.projetando.net

8

XmlAnyEment Campo público, propriedade, parâmetro ou valor de retorno que retorna um array de objetos do tipo XmlElemet

Quando deserializado, o array será carregado com objetos do tipo XmlElemet que representam todos os elementos XML desconhecidos do schema.

XmlArray Campo público, propriedade, parâmetro ou valor de retorno que retorna um array de objetos complexos

Os membros do array serão gerados como membros do array XML.

XmlArrayItem Campo público, propriedade, parâmetro ou valor de retorno que retorna um array de objetos complexos

Os tipos derivados que podem ser inseridos dentro array. Usualmente é aplicado junto com um XmlArray.

XmlAttribute Campo público, propriedade, parâmetro ou valor de retorno

O membro será serializado como um atributo XML.

XmlChoiceIdentifier Campo público, propriedade, parâmetro ou valor de retorno

Especifica que membro pode ser futuramente detectado utilizando um enumerador.

XmlElement Campo público, propriedade, parâmetro ou valor de retorno

O campo ou a propriedade será serializado como um elemento XML.

XmlEnum Campo público que é um enumerador

Aplicado a enumeradores para customizar como seus valores serão serializados.

XmlIgnore Campos e propriedades públicos

A propriedade ou o campo será ignorado quando a classe for serializada.

XmlInclude Classes derivadas e retorno de métodos públicos para documentos WSDL

Um determinado tipo deve ser incluído quando for gerar schemas e, conseqüentemente, ser reconhecido quando for serializado.

XmlRoot Classes públicas Controla a serialização XML do atributo, definindo ele como sendo o root.

XmlText Campos e propriedades públicos

O campo ou a propriedade deve ser serializada como um texto XML.

XmlType Classes públicas O nome e namespace do tipo XML.

Observação: O sufixo Attribute foi removido da coluna Atributo por questões de espaço. Apesar de existir, o compilador consegue entender e torna o sufixo não obrigatório quando é utilizado.

Page 194: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 9

Israel Aece | http://www.projetando.net

9

Para exemplificar a utilização de algum desses atributos vamos analisar trechos do código que estará disponível na íntegra na demonstração do capítulo. Através do código abaixo veremos a utilização dos atributos XmlRoot, XmlAttribute e XmlIgnore. O XmlRoot vai determinar qual será o elemento raiz do documento XML. O construtor deste atributo pode receber uma string contendo o nome mas, se nenhum for informado, o mesmo nome do membro será definido como raiz. Já o segundo atributo, XmlAttribute, é utilizado para marcarmos uma propriedade ou campo como sendo um atributo ao invés de um elemento. Finalmente, o XmlIgnore que irá evitar de serializar um determinado campo do objeto. VB.NET Imports System.Xml Imports System.Xml.Serialization <XmlRoot("funcionariosAtivos"), Serializable> _ Public Class Empresa Private _sala As String ‘... <XmlAttribute("salaDoEscritorio")> _ Public Property Sala As String ‘... End Property End Class <Serializable> _ Public Class Funcionario Private _salario As Double ‘... <XmlIgnore> _ Public Property Salario As Double ‘... End Property End Class C# using System.Xml; using System.Xml.Serialization; [Serializable] [XmlRoot("funcionariosAtivos")] public class Empresa { private string _sala;

Page 195: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 10

Israel Aece | http://www.projetando.net

10

//... [XmlAttribute("salaDoEscritorio")] public string Sala { //... } } [Serializable] public class Funcionario { private double _salario; //... [XmlIgnore] public double Salario { //... } } O documento XML gerado é exibido abaixo. Se analisarmos, o elemento raiz chama-se “funcionariosAtivos”, assim como definimos através do elemento XmlRoot. Repare também que a propriedade Sala foi serializada com o nome “salaDoEscritorio” e sendo atributo do elemento “funcionariosAtivos”. Finalmente, a propriedade Salario não é persitido, justamente porque o atributo XmlIgnore foi especificado nele. XML <?xml version="1.0"?> <funcionariosAtivos xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" salaDoEscritorio="Oitavo andar"> <Funcionarios> <Funcionario> <Nome>João Mendes</Nome> <Superior> <Nome>Mateus Golias</Nome> </Superior> </Funcionario> </Funcionarios> </funcionariosAtivos>

Page 196: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 11

Israel Aece | http://www.projetando.net

11

Customizando a serialização XML Se desejarmos customizar a serialização e deserialização realizada pela classe XmlSerializer, podemos implementar a Interface IXmlSerializable na classe que será persistida. Essa Interface fornece três métodos: GetSchema, ReadXml e WriteXml. O primeiro deles, retorna um objeto do tipo XmlSchema que descreve o documento XML que é gerado pelo método WriteXml e lido pelo método ReadXml. Já os métodos ReadXml e WriteXml lê e gera o documento XML, respectivamente. Para o método ReadXml é passado como parâmetro um objeto do tipo XmlReader, que é o stream que representa o objeto serializado. Esse método deve ser capaz de extrair as informações do objeto e reconstituí-lo. Já para o método WriteXml, um objeto do tipo XmlWriter é passado como parâmetro. Esse parâmetro fornecerá métodos para que seja possível persistir o objeto em formato XML. A classe CPF abaixo implementa a Interface IXmlSerializable e customizada cada um dos métodos de uma forma simples para fins de exemplo. VB.NET Imports System.Xml Imports System.Xml.Serialization Imports System.Xml.Schema Public Class CPF Implements IXmlSerializable Private _numero As String Public Property Numero() As String Get Return Me._numero End Get Set(ByVal value As String) Me._numero = value End Set End Property Public Function GetSchema() As XmlSchema _ Implements IXmlSerializable.GetSchema Return Nothing End Function Public Sub ReadXml(ByVal reader As XmlReader) _ Implements IXmlSerializable.ReadXml Me._numero = reader.ReadString() End Sub

Page 197: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 12

Israel Aece | http://www.projetando.net

12

Public Sub WriteXml(ByVal writer As XmlWriter) _ Implements IXmlSerializable.WriteXml writer.WriteString(Me._numero) End Sub End Class C# using System.Xml; using System.Xml.Serialization; using System.Xml.Schema; public class CPF : IXmlSerializable { private string _numero; public string Numero { get { return this._numero; } set { this._numero = value; } } public XmlSchema GetSchema() { return null; } public void ReadXml(XmlReader reader) { this._numero = reader.ReadString(); } public void WriteXml(XmlWriter writer) { writer.WriteString(this._numero); } } Se repararmos, quando a Interface IXmlSerializable é implementada, a classe dispensa o uso do atributo Serializable. É importante dizer também que a forma como se faz a serialização através do objeto XmlSerializer não muda absolutamente nada.

Page 198: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 13

Israel Aece | http://www.projetando.net

13

A classe XmlSerializer ainda fornece quatro eventos interessantes que são invocados durante a deserialização do objeto. Quando serializamos um determinado objeto, persistimos as informações necessárias que desejamos que sejam armazenadas. Suponhamos que mais tarde, quando queremos recuperar esse objeto através do processo de deserialização, o arquivo em que o persistimos tiver atributos ou elementos desconhecidos, ou seja, membros que não fazem parte do schema atual do objeto, eles serão ignorados pelo processo de deserialização. Apesar de ignorados pelo processo, podemos ser notificados quando isso acontecer e isso é possível através desses quatro eventos que falamos. A tabela abaixo explica detalhadamente cada um deles: Evento Descrição UnknownAttribute Ocorre quando o objeto XmlSerializer encontrar um atributo

desconhecido durante o processo de deserialização. UnknownElement Ocorre quando o objeto XmlSerializer encontrar um elemento

desconhecido durante o processo de deserialização. UnknownNode Ocorre quando o objeto XmlSerializer encontrar um nó

desconhecido durante o processo de deserialização. UnreferencedObject Ocorre quando durante o processo de deserialização de um stream

baseado em SOAP, quando o objeto XmlSerializer encontrar um tipo conhecido que não é usado ou referenciado.

Para que seja possível interceptar o lançamento desses eventos, é necessário anexarmos os procedimentos que serão disparados quando o mesmo for disparado. Temos abaixo um exemplo simples de como proceder neste caso: VB.NET Imports System.Xml.Serialization Using stream As Stream = File.Open(fileName, FileMode.Open) Dim f As New XmlSerializer(GetType(CPF)) AddHandler f.UnknownAttribute, AddressOf UnknownAttribute AddHandler f.UnknownElement, AddressOf UnknownElement AddHandler f.UnknownNode, AddressOf UnknownNode Dim c As CPF = TryCast(f.Deserialize(stream), CPF) End Using Private Sub UnknownAttribute(ByVal sender As Object, _ ByVal e As XmlAttributeEventArgs) Console.WriteLine(e.Attr.Name) Console.WriteLine(e.LineNumber) End Sub Private Sub UnknownNode(ByVal sender As Object, _ ByVal e As XmlNodeEventArgs)

Page 199: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 14

Israel Aece | http://www.projetando.net

14

Console.WriteLine(e.Name) Console.WriteLine(e.LineNumber) End Sub Private Sub UnknownElement(ByVal sender As Object, _ ByVal e As XmlElementEventArgs) Console.WriteLine(e.Element.Name) Console.WriteLine(e.LineNumber) End Sub C# using System.Xml.Serialization; using (Stream stream = File.Open(fileName, FileMode.Open)) { XmlSerializer f = new XmlSerializer(typeof(T)); f.UnknownElement += new XmlElementEventHandler(UnknownElement); f.UnknownNode += new XmlNodeEventHandler(UnknownNode); f.UnknownAttribute += new XmlAttributeEventHandler(UnknownAttribute); CPF c = f.Deserialize(stream) as CPF; } static void UnknownAttribute(object sender, XmlAttributeEventArgs e) { Console.WriteLine(e.Attr.Name); Console.WriteLine(e.LineNumber); } static void UnknownNode(object sender, XmlNodeEventArgs e) { Console.WriteLine(e.Name); Console.WriteLine(e.LineNumber); } static void UnknownElement(object sender, XmlElementEventArgs e) { Console.WriteLine(e.Element.Name); Console.WriteLine(e.LineNumber); }

Page 200: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 15

Israel Aece | http://www.projetando.net

15

Serialização customizada Quanto mais você trabalha com serialização e deserialização de objetos para enviá-los para os mais diversos locais, mais aumenta a necessidade de customizar esses processos para atender uma determinada funcionalidade. Como vimos até o momento, podemos utilizar as classes fornecidas pelo .NET Framework para efetuarmos a persistência, mas se estivermos falando de um cenário muito específico, felizmente como grande parte do .NET Framework, ele também fornece classes e Interfaces para customizarmos os processos de serialização e deserialização, como por exemplo, podemos customizar um formatter específico para que ele atenda ao nosso cenário. Já notamos que durante o processo de serialização, precisaremos especificar quais valores serão salvos e, quando fazer o inverso, o processo de deserialização, devemos extrair os valores do stream para o nosso objeto corrente. O .NET Framework fornece duas estruturas SerializationEntry, StreamingContext e a classe SerializationInfo. Esses tipos são utilizados para especificar os valores que são requeridos para representar o objeto na sua forma serializada e recuperá-los durante o processo de deserialização. Além disso, esses tipos fornecem informações a respeito do tipo que foi serializado, Assembly, etc.. Para um detalhamento melhor, a estrutura SerializationEntry contém as propriedades Name, ObjectType e Value, quais disponibilizam o nome, tipo e valor, respectivamente, do objeto serializado. Já a estrutura StreamingContext fornece informações a respeito da fonte e do destino do stream, ou seja, durante o processo de serialização, define o destino dos dados e, quando for o processo de deserialização, especifica a fonte do stream. Finalmente a classe SerializationInfo, que está contida dentro do namespace System.Runtime.Serialization, armazena todo o conteúdo que é necessário tanto para serializar quanto para deserializar um determinado objeto em uma coleção do tipo chave-valor. Interfaces Como dissemos há pouco tempo atrás, o .NET Framework disponibiliza várias Interfaces que podemos utilizar para customizar os processos de serialização e deserialização. Essas Interfaces estão contidas dentro do namespace System.Runtime.Serialization. Essas Interfaces, quando implementadas, permitem um melhor controle sob como os objetos são serializados e deseriliazados. As Interfaces que temos a nossa disposição são: ISerializable, IFormatter, IFormatterConverter e IDeserializationCallback. Abaixo analisaremos com mais detalhes cada uma delas e qual a sua utilidade.

Page 201: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 16

Israel Aece | http://www.projetando.net

16

ISerializable Qualquer classe pode ser seralizada desde que esteja marcada com o atributo Serializable. Se a classe precisa controlar o processo de serialização, você pode implementar a Interface ISerializable. Por padrão, o formatter que está sendo utilizado para o processo de serialização invoca o método GetObjectData (fornecido pela Interface em questão) e fornece um objeto do tipo SerializationInfo com todos os dados que são requeridos para representar o objeto. A implementação desta Interface implica em a classe possuir um construtor que recebe como parâmetro um objeto do tipo SerializationInfo e outro do tipo StreamingContext. Durante o processo de deserialização, esse construtor é invocado somente depois que os dados foram deserializados pelo formatter. Geralmente esse construtor deve ser protected se a classe permitir derivações. O techo de código abaixo ilustra a utilização da implementação desta Interface: VB.NET Imports System.Runtime.Serialization Public Class CPF Implements ISerializable Private _numero As Integer Private _nome As String Public Sub New() End Sub Protected Sub New(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) Me._numero = info.GetInt32("numero") Me._nome = info.GetString("nome") End Sub ‘ Propriedades que expõe os membros internos ‘ _numero e _nome Public Sub GetObjectData(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) _ Implements ISerializable.GetObjectData info.AddValue("numero", Me._numero) info.AddValue("nome", Me._nome) End Sub End Class

Page 202: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 17

Israel Aece | http://www.projetando.net

17

C# using System.Runtime.Serialization; public class CPF : ISerializable { private int _numero; private string _nome; public CPF() { } protected CPF(SerializationInfo info, StreamingContext context) { this._numero = info.GetInt32("numero"); this._nome = info.GetString("nome"); } // Propriedades que expõe os membros internos // _numero e nome public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("numero", this._numero); info.AddValue("nome", this._nome); } } IFormatter A Interface IFormatter por qualquer classe que irá ser um formatter, fornecendo funcionalidades para formatar objetos serializados e também possibilitando o controle do output dos processos de serialização e deserialização. Essa Interface fornece os dois principais métodos que são utilizados por um formatter: Serialize e Deserialize. Além de disponibilizar todos os membros necessários para a criação de um formatter customizado, essa Interface também é implementada em uma classe abstrata chamada Formatter. Essa classe fornece funcionalidades básicas e pode ser utilizada (herdada) para todos os formatters customizados que forem construídos ao invés de implementar diretamente a Interface IFormatter. Neste caso, todos os membros que são herdados da Interface IFormatter são mantidos como abstratos; ela somente adiciona outros membros que auxiliam durante os processos. IFormatterConverter Essa Interface fornece uma conexão entre a instância da classe SerializationInfo e o formatter para que seja possível efetuar as conversões de tipos necessárias durante os

Page 203: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 18

Israel Aece | http://www.projetando.net

18

processos. Assim como a Interface IFormatter é implementada na classe abstrata Formatter mas, Interface IFormatter também é implementada em uma classe chamada FormatterConverter, contendo toda a implementação básica para o código necessário para efetuar as conversões. Internamente, essa classe faz o uso da classe Convert. IDeserializationCallback Vamos imaginar o seguinte cenário: temos uma classe chamada Calculo que, dados dois números ele calcula a soma entre eles. Naturalmente quando optar por serializar esse objeto, você não irá armazenar o valor do resultado, ou seja, vai apenas se preocupar em salvar os valores que efetivamente geram o resultado. Até o momento, nada diferente. Como propriedades públicas são, por padrão, persistidas automaticamente, não teremos problemas com relação a perda dos dados. Mas, e se no momento da deserialização quisermos notificar o objeto recém “revitalizado” para que ele faça alguma operação? É neste momento que a Interface IDeserializationCallback entra em ação. Quando o método Deserialize do formatter é chamado, internamente ele verifica se o tipo do objeto que está sendo recuperado implementa ou não essa Interface. Se estiver implementada, o método OnDeserialization, fornecido por ela, é executado. Trazendo isso para o nosso exemplo, poderemos nesse momento, efetuarmos o cálculo (soma) dos números para quando o usuário recuperar o valor, o mesmo já estar processado. Para exemplificar a utilização desta Interface, o código abaixo exibe apenas os pontos relevantes do código necessários para essa implementação: VB.NET Imports System.Runtime.Serialization <Serializable()> Public Class Soma Implements IDeserializationCallback Private _numero1 As Integer Private _numero2 As Integer Private _total As Integer ‘ propriedades ocultadas por ‘ questões de espaço Public Sub OnDeserialization(ByVal sender As Object) _ Implements IDeserializationCallback.OnDeserialization Me._total = Me._numero1 + Me._numero2 End Sub End Class C# using System.Runtime.Serialization;

Page 204: Livro de .NET - Israel Aece

Capítulo 6 - Serialização 19

Israel Aece | http://www.projetando.net

19

[Serializable] public class Soma : IDeserializationCallback { private int _numero1; private int _numero2; private int _total; // propriedades ocultadas por // questões de espaço public void OnDeserialization(object sender) { this._total = this._numero1 + this._numero2; } } Quando o usuário requisitar pelo membro _total ele já conterá o resultado da soma entre os dois membros internos.

Page 205: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 1

Israel Aece | http://www.projetando.net

1

Capítulo 7 Globalização de Aplicações Introdução Atualmente é muito comum as empresas estarem expandindo seus negócios entre vários países do mundo. Além disso, ainda há casos onde pessoas estrangeiras são comumente contratados por essas empresas. Com isso, o software ou até mesmo o website da empresa, deverá contemplar vários idiomas. Para possibilitar isso, as aplicações devem ser capazes de configurar o padrão e formatação dos números (isso inclui o sistema monetário) e datas e, principalmente, serem capazes de ajustar a interface com usuário, pois em determinados idiomas, podem exigir mais espaço para uma determinada informação e, se a aplicação não previnir isso, poderá deixar a tela inutilizálvel. Neste capítulo veremos detalhadamente como devemos proceder para configurarmos uma aplicação para que a mesma suporte as funcionalidades de globalização, analisando as classes e tipos fornecidos pelo namespace System.Globalization. Globalização e Localização Quando estamos falando de aplicações para múltiplos idiomas e países, temos que entender dois aspectos muito importantes que são a globalização e localização:

• Globalização: O processo de globalização é a habilidade de construir aplicações/websites que são suportadas e adaptáveis para as mais diferentes culturas.

• Localização: É a habilidade de localizar a aplicação para uma cultura e região específica, criando traduções para os recursos que a aplicação utiliza em seu interior. Um exemplo típico é a localização de uma aplicação/website para o português para várias regiões, como o Brasil (pt-BR) e Portugal (pt-PT).

Como podemos notas na descrição acima, as culturas são identificadas por um padrão universal, contendo duas partes, sendo a primeira delas dois caracteres minúsculos que identificam o idioma. Já na segunda parte, existem mais dois caracteres maiúsculos que representam o país. Existem uma enorme lista com todos as culturas suportadas pelo .NET Framework, mas que não será exibida aqui por questões de espaço. Mas se desejar consultar, basta localizar a classe CultureInfo no MSDN Library local ou no site e lá terá a lista completa. Quando trabalhamos com culturas, temos dois conceitos importantes que devemos levar em consideração. Trata-se da cultura corrente e da cultura de interface (uiculture). A

Page 206: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 2

Israel Aece | http://www.projetando.net

2

primeira delas, é utilizada para operarmos com formatação de datas e números e, além disso, utilizada durante a escrita do código; já a segunda, é utilizada pelo Resource Manager para analisar uma cultura específica e recuperar os recursos em tempo de execução. Para definirmos cada uma dessas culturas, utilizamos as propriedades CurrentCulture e CurrentUICulture, respectivamente. Essas propriedades são estáticas e estão contidas dentro da classe Thread que, por sua vez, está dentro do namespace System.Threading. Utilizando Culturas Uma das classes essencias na criação de aplicações globalizadas, cada instância da classe CultureInfo representa uma cultura específica, contendo informações específicas da cultura que ela representa e, entre essas informações, temos o nome da cultura, sistema de escrita, calendários, como formatar datas, etc.. A tabela abaixo exibe os principais métodos e propriedades desta classe. Membro Descrição Calendar Propriedade de somente leitura que retorna um objeto do tipo

Calendar. O objeto Calendar representa as divisões do tempo, como semanas, meses e anos.

ComparerInfo Propriedade de somente leitura que retorna um objeto do tipo ComparerInfo que define como comparar as strings para a cultura corrente.

CultureTypes Propriedade de somente leitura que retorna uma combinação do enumerador CultureTypes, indicando à que tipo a cultura corrente pertence. Entre as opções fornecidas pelo enumerador CultureTypes, temos:

• AllCultures – Todas as culturas, incluindo as culturas que fazem parte do .NET Framework (neutras e específicas), culturas instaladas no Windows e as culturas customizadas, criadas pelo usuário.

• FrameworkCultures – Culturas específicas e neutras que fazem parte do .NET Framework.

• InstalledWin32Cultures – Todas as culturas instaladas dentro do Windows.

• NeutralCultures – Culturas que estão associadas com um idioma mas não com uma região/país específico.

• ReplacementCultures – Culturas customizadas pelo usuário que substituem as culturas disponibilizadas pelo .NET Framework.

• SpecificCultures – Culturas que não são específicas para uma região/país.

• UserCustomCulture – Culturas customizadas, criadas pelo usuário.

Page 207: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 3

Israel Aece | http://www.projetando.net

3

• WindowsOnlyCultures – Somente as culturas instaladas dentro do Windows e não fazem parte do .NET Framework.

CurrentCulture Propriedade estática de somente leitura que retorna um objeto do tipo CultureInfo que está sendo utilizada pela thread corrente. Essa propriedade nada mais é que um wrapper para a propriedade estática CurrentCulture da classe Thread.

CurrentUICulture Propriedade estática de somente leitura que retorna um objeto do tipo CultureInfo que está sendo utilizada pelo Resource Manager para extrair os recursos em tempo de execução. Essa propriedade nada mais é que um wrapper para a propriedade estática CurrentUICulture da classe Thread.

DateTimeFormat Propriedade de somente leitura que retorna um objeto do tipo DateTimeFormatInfo que define as formas apropriadas para exibir e formatar datas e horas para a cultura corrente.

DisplayName Propriedade de somente leitura que retorna uma string com o nome da cultura no formato full, exemplo: en-US.

EnglishName Propriedade de somente leitura que retorna uma string com o nome da cultura em inglês.

InstalledUICulture Propriedade estática de somente leitura que retorna um objeto do tipo CultureInfo que representa a cultura instalada com o sistema operacional.

IsNeutralCulture Propriedade de somente leitura que retorna um valor booleano indicando se o objeto CultureInfo corrente representa uma cultura neutra.

IsReadOnly Propriedade de somente leitura que retorna um valor booleano indicando se o objeto CultureInfo corrente é ou não somente leitura.

Name Propriedade de somente leitura que retorna uma string contendo o nome da cultura corrente no seguinte formato: English (United States).

NativeName Propriedade de somente leitura que retorna uma string contendo o nome da cultura corrente em seu idioma atual: English (United States).

NumberFormat Propriedade de somente leitura que retorna um objeto do tipo NumberFormatInfo que define as formas apropriadas para exibir e formatar números (inclusive o sistema monetário, porcentagens) para a cultura corrente.

Parent Propriedade de somente leitura que retorna um objeto do tipo CultureInfo que representa a cultura “pai” da cultura corrente.

TextInfo Propriedade de somente leitura que retorna um objeto do tipo TextInfo que define a forma de escrita associada com a cultura corrente.

UseUserOverride Propriedade de somente leitura que retorna um valor booleano indicando se o objeto CultureInfo corrente utiliza as opções de

Page 208: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 4

Israel Aece | http://www.projetando.net

4

culturas definidas pelo usuário através das Configurações Regionais do Painel de Controle do Windows.

CreateSpecificCulture Método estático que, dado uma cultura específica, cria e retorna um objeto do tipo CultureInfo associado com a cultura informada.

GetCultureInfo Método estático que, dado uma cultura específica, retorna uma instância do objeto CultureInfo (read-only) associado com a cultura informada.

GetCultures Método estático que retorna um array de culturas, onde cada um dos elementos é representado por um objeto do tipo CultureInfo.

GetFormat Este método, através de um objeto do tipo Type, retorna uma instância de um formatador associadao com a cultura corrente. Esse método somente aceita como parâmetro um objeto Type que representa a classe NumberFormatInfo ou a classe DateTimeFormatInfo. Do contrário, esse método retornará nulo.

O código abaixo exibe a forma de criação e a exibição de algumas das propriedades do objeto CultureInfo: VB.NET Imports System.Globalization Sub Main() Dim pt As New CultureInfo("pt-BR") Dim en As CultureInfo = CultureInfo.CreateSpecificCulture("en-US") Show(New CultureInfo() {pt, en}) End Sub Private Sub Show(ByVal cultures() As CultureInfo) For Each ci As CultureInfo In cultures Console.WriteLine("------------------------") Console.WriteLine(ci.DisplayName) Console.WriteLine(ci.DateTimeFormat.DateSeparator) Console.WriteLine(ci.DateTimeFormat.FirstDayOfWeek.ToString()) Console.WriteLine(ci.NumberFormat.CurrencyDecimalSeparator) Next End Sub C# using System.Globalization;

Page 209: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 5

Israel Aece | http://www.projetando.net

5

static void Main(string[] args) { CultureInfo pt = new CultureInfo("pt-BR"); CultureInfo en = CultureInfo.CreateSpecificCulture("en-US"); Show(new CultureInfo[] { pt, en }); } static void Show(CultureInfo[] cultures) { foreach (CultureInfo ci in cultures) { Console.WriteLine("------------------------"); Console.WriteLine(ci.DisplayName); Console.WriteLine(ci.DateTimeFormat.DateSeparator); Console.WriteLine(ci.DateTimeFormat.FirstDayOfWeek.ToString()); Console.WriteLine(ci.NumberFormat.CurrencyDecimalSeparator); } } A única diferença entre a criação do objeto pt e en é que no primeiro, pt, foi criado a partir da instância da classe CultureInfo; já com o en, optamos por criar a partir do método estático CreateSpecificCulture da classe CultureInfo. O resultado para ambos os códigos são idênticos e é exibido abaixo: ------------------------ Portuguese (Brazil) / Sunday , ------------------------ English (United States) / Sunday . Recuperando informações de uma região (país) Existe uma classe dentro do namespace System.Globalization chamada RegionInfo. Ao contrário da classe CultureInfo, ela não representa as preferências do usuário e não depende do idioma ou cultura do mesmo. A classe RegionInfo fornece informações referente a uma região/país especifíco.

Page 210: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 6

Israel Aece | http://www.projetando.net

6

Essa classe contém um overload que recebe uma string. Essa string deve conter o nome da região que você deseja recuperar as informações e, esse nome, deve ser dois caracteres maiúsculos de acordo com o padrão estabelecido pela ISO. A tabela completa pode ser consultada quando você abre a documentação da classe RegionInfo. Entre as principais propriedades desta classe tempos (com os exemplos baseados em uma instância da classe RegionInfo que representa o Brasil): Propriedade Descrição CurrencyEnglishName Propriedade de somente leitura que retorna uma string contendo

o nome, em inglês, da moeda utilizada pela região corrente. Exemplo: Real

CurrencyNativeName Propriedade de somente leitura que retorna uma string contendo o nome no idioma nativo da região corrente. Exemplo: Real

CurrencySymbol Propriedade de somente leitura que retorna uma string contendo o símbolo monetário utilizado pela região corrente. Exemplo: R$

CurrentRegion Propriedade estática de somente leitura que retorna uma instância da classe RegionInfo representando a região da thread corrente.

DisplayName Propriedade de somente leitura que retorna uma string contendo o nome da região corrente no idioma localizado do .NET Framework. Exemplo: Brazil

EnglishName Propriedade de somente leitura que retorna uma string contendo o nome, em inglês, da região corrente. Exemplo: Brazi

Name Propriedade de somente leitura que retorna uma string com o nome da região corrente. Esse nome é representado por dois caracteres maiúsculos. Exemplo: BR

NativeName Propriedade de somente leitura que retorna uma string contendo o nome da região corrente em seu idioma nativo. Exemplo: Brasil.

Logo abaixo temos um exemplo da utilização desta classe: VB.NET Imports System.Globalization Dim r As New RegionInfo("BR") Console.WriteLine(r.CurrencyNativeName) Console.WriteLine(r.CurrencySymbol) Console.WriteLine(r.NativeName) C# using System.Globalization; RegionInfo r = new RegionInfo("BR");

Page 211: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 7

Israel Aece | http://www.projetando.net

7

Console.WriteLine(r.CurrencyNativeName); Console.WriteLine(r.CurrencySymbol); Console.WriteLine(r.NativeName); Formatação de Datas e Números Em alguns momentos acima vimos mencionados as classes DateTimeFormatInfo e NumberFormatInfo. Essas classes fornecem uma grande funcionalidade, que é a formatação e padronização de datas, horas e números dentro da plataforma .NET. Essas classes fornecem informações específicas para a manipulação de datas, horas e números em diferentes regiões e, como vimos, a classe CultureInfo disponibiliza propriedades que retornam a instância dessas respectivas classes já com a cultura especificada mas, nada impede que você crie a sua própria instância e customize as suas propriedades necessárias. A classe DateTimeFormatInfo Como falamos acima, a classe DateTimeFormatInfo irá auxiliar na formatação de data e hora de acordo com a cultura selecionada. Todas as propriedades que essa classe possui já refletem as informações da cultura específica quando você extrai a instância dessa classe a partir do método GetFormat ou da propriedade DateTimeFormat da classe CultureInfo. Essa classe também suporta um padrão pré-determinado, onde podemos especificar alguns caracteres que determinam a formatação da data/hora que será exibido. Alguns desses especificadores são mostrados através da tabela abaixo: Especificador Descrição d Especifica um padrão para a exibição de uma data de uma forma

reduzida. É um atalho para a propriedade ShortDatePattern da classe DateTimeFormatInfo.

D Especifica um padrão para a exibição de uma data de uma forma mais completa. É um atalho para a propriedade LongDatePattern da classe DateTimeFormatInfo.

f Especifica um padrão para a exibição de uma hora de uma forma reduzida. É um atalho para a propriedade ShortTimePattern da classe DateTimeFormatInfo.

F Especifica um padrão para a exibição de uma hora de uma forma mais completa. É um atalho para a propriedade LongTimePattern da classe DateTimeFormatInfo.

t Exibe uma combinação da data em seu formato completo e a hora em sua forma reduzida.

T Exibe uma combinação da data em seu formato completo e a hora em sua forma completa. É um atalho para a propriedade

Page 212: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 8

Israel Aece | http://www.projetando.net

8

FullDateTimePattern da classe DateTimeFormatInfo. Além dos padrões que vimos na tabela acima, ainda há a possibilidade de, com esses mesmos caracteres, combiná-los para customizarmos como a data/hora será exibida. A estrutura DateTime fornece alguns métodos que serve como wrapper para as propriedades que citamos acima e, além disso, o método ToString desta estrutura possui alguns overloads que permitem customizarmos a formatação. Para testarmos as formatações acima, vamos utilizá-la no exemplo a seguir: VB.NET Dim hoje As DateTime = DateTime.Now Console.WriteLine(hoje.ToString("d")) Console.WriteLine(hoje.ToString("D")) Console.WriteLine(hoje.ToString("f")) Console.WriteLine(hoje.ToString("F")) Console.WriteLine(hoje.ToString("t")) Console.WriteLine(hoje.ToString("T")) Console.WriteLine(hoje.ToString("dd/MM/yyyy")) C# DateTime hoje = DateTime.Now; Console.WriteLine(hoje.ToString("d")); Console.WriteLine(hoje.ToString("D")); Console.WriteLine(hoje.ToString("f")); Console.WriteLine(hoje.ToString("F")); Console.WriteLine(hoje.ToString("t")); Console.WriteLine(hoje.ToString("T")); Console.WriteLine(hoje.ToString("dd/MM/yyyy")); Como resultado obtemos: 31/3/2007 sábado, 31 de março de 2007 sábado, 31 de março de 2007 18:23 sábado, 31 de março de 2007 18:23:13 18:23 18:23:13 31/03/2007 Na última linha do exemplo, utilizamos um overload do método ToString que permite passarmos uma combinação de alguns caracteres que permite-nos customizar a formatação da data/hora. No exemplo MM significa que se trata de mês com duas casas. Atente-se, pois mm (minúsculos) trata-se de minutos.

Page 213: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 9

Israel Aece | http://www.projetando.net

9

Quando fazemos a formatação da forma acima, por padrão, o DateTimeFormatInfo que é utilizado é extraído através da propriedade estática CurrentInfo da mesma. Vale lembrar que essa propriedade nada mais é que um wrapper para a propriedade CurrentCulture da thread corrente. Mas podemos criar instâncias da nossa própria classe DateTimeFormatInfo para customizarmos como a data/hora será tratada. Antes de analisarmos como devemos proceder para utilizá-la, vamos entender um pouco melhor algumas das propriedades que ela nos fornece através da tabela abaixo: Membro Descrição AbbreviateDayNames Propriedade que retorna um array de strings, onde cada

elemento do mesmo corresponde a um dia em sua forma abreviada.

CurrentInfo Propriedade estática de somente leitura que retorna um objeto do tipo DateTimeFormatInfo da thread atual.

DateSeparator Uma string que determina qual será o separador das datas. DayNames Propriedade que retorna um array de strings, onde cada

elemento do mesmo corresponde a um dia com o seu nome completo.

FirstDayOfWeek Propriedade que podemos definir qual será o primeiro dia da semana. Essa propriedade definimos com alguma opção do enumerador DayOfWeek. As opções fornecidas por esse enumerador são:

• Friday – Indica sexta-feira. • Saturday – Indica sábado. • Sunday – Indica domingo. • Monday – Indica segunda-feira. • Thursday – Indica terça-feira. • Tuesday – Indica quinta-feira. • Wednesday – Indica quarta-feira.

FullDateTimePattern Propriedade onde definimos o formato da data e hora em seu formato longo. Este padrão está associado ao caracter “F” que vimos acima.

MonthDayPattern Propriedade onde definimos o formato do dia e mês, que estão associados com os caracteres “d” e “M”.

MonthNames Propriedade que retorna um array de strings, onde cada elemento do mesmo corresponde a um mês com o seu nome completo.

TimeSeparator Uma string que determina qual será o separador de horas. YearMonthPattern Propriedade onde definimos o formato do ano e mês, que estão

associados com os caracteres “y” e “Y”.

Page 214: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 10

Israel Aece | http://www.projetando.net

10

Como dissemos acima, podemos criar uma instância dessa classe customizarmos como desejarmos. Vemos mais utilidade nisso quando estamos criando uma cultura customizada, que analisaremos mais adiante. Abaixo está a forma que devemos proceder para a criação deste objeto: VB.NET Dim dtfi As New DateTimeFormatInfo() dtfi.DateSeparator = "|" dtfi.TimeSeparator = "." Console.WriteLine(DateTime.Now.ToString(dtfi)) C# DateTimeFormatInfo dtfi = new DateTimeFormatInfo(); dtfi.DateSeparator = "|"; dtfi.TimeSeparator = "."; Console.WriteLine(DateTime.Now.ToString(dtfi)); É importante notar que um dos overloads do método ToString da estrutura DateTime recebe um tipo IFormatProvider e, como a classe DateTimeFormatInfo implementa essa Interface, ela é pode ser passada para o mesmo. O resultado do código é exibido abaixo: 04|02|2007 10.19.49 A classe NumberFormatInfo Assim como a classe DateTimeFormatInfo, a classe NumberFormatInfo é responsável por tratar da formatação de números dentro da plataforma .NET, ainda estendendo para o sistema monetário da região corrente. Essa classe também fornece alguns caracteres, com padrões pré-determinados que podemos utilizar para customizar a formatação de um determinado valor. Além disso, ela fornece também propriedades que podemos definir os símbolos, separadores decimais, etc. Através da tabela abaixo, vamos analisar os caracteres que temos disponíveis para a formatação de valores numéricos: Caracter Descrição c, C Formato monetário. d, D Formato decimal. e, E Formato científico (exponencial). f, F Formato fixo. g, G Formato padrão.

Page 215: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 11

Israel Aece | http://www.projetando.net

11

n, N Formato numérico. r, R Formato roundtrip. Esse formato assegura que os números convertidos em

strings terão o mesmo valor quando eles forem convertidos de volta para números.

x, X Formato hexadecimal. Com o conhecimento desses caracteres, já é possível utilizá-los no overload do método ToString da estrutura Double, onde podemos determinar qual a forma que o valor contido dentro dela será exibido. Além do caracter, ainda é possível determinar a quantidade de casas decimais que será exibido, usando em conjunto com o caracter de formatação um número que irá informar quantas casas decimais deverá ter: VB.NET Dim valor As Double = 123.2723 Console.WriteLine(valor.ToString("C2")) Console.WriteLine(valor.ToString("N3")) C# Double valor = 123.2723; Console.WriteLine(valor.ToString("C2")); Console.WriteLine(valor.ToString("N3")); O resultado desse código é mostrado abaixo: R$ 123,27 123,272 Mais uma vez, é importante dizer que quando utilizamos as formatações dessa forma, apesar de explicitamente não estarmos utilizando a classe NumberFormatInfo, ela é extraída automaticamente da thread corrente e, conseqüentemente, formatando os valores baseando-se nessas informações. Para conhecermos um pouco mais sobre a classe NumberFormatInfo, vamos analisar algumas das propriedades que elas nos fornece através da tabela abaixo: Propriedade Descrição CurrencyDecimalDigits Propriedade que recebe um número inteiro indicando

quantas casas decimais é utilizada em valores monetários. CurrencyDecimalSeparator Propriedade que recebe uma string contendo o caracter que

será utilizado como separador de casas decimais. CurrencyGroupSeparator Propriedade que recebe uma string contendo o caracter que

será utilizado como separador dos grupos de números. É

Page 216: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 12

Israel Aece | http://www.projetando.net

12

utilizado entre os grupos de números. CurrencyGroupSizes Propriedade que recebe um array de números inteiros que

representam qual será o comprimento de cada grupo. CurrentInfo Propriedade estática de somente leitura que retorna um

objeto do tipo NumberFormatInfo da thread atual. NegativeSign Propriedade que recebe uma string contendo o caracter que

será associado ao número quando ele for negativo. NumberDecimalDigits Propriedade que recebe um número inteiro indicando

quantas casas decimais é utilizada em números em geral. NumberDecimalSeparator Propriedade que recebe uma string contendo o caracter que

será utilizado como separador de casas decimais para números em geral.

NumberGroupSeparator Propriedade que recebe uma string contendo o caracter que será utilizado como separador dos grupos de números. É utilizado entre os grupos de números.

NumberGroupSizes Propriedade que recebe um array de números inteiros que representam qual será o comprimento de cada grupo.

PositiveSign Propriedade que recebe uma string contendo o caracter que será associado ao número quando ele for positivo.

Como dissemos acima, podemos criar uma instância dessa classe customizarmos como desejarmos. Vemos mais utilidade nisso quando estamos criando uma cultura customizada, que analisaremos mais adiante. Abaixo está a forma que devemos proceder para a criação do objeto NumberFormatInfo: VB.NET Dim nfi As New NumberFormatInfo() nfi.CurrencyDecimalDigits = 3 nfi.CurrencyDecimalSeparator = "*" nfi.CurrencyGroupSeparator = "_" nfi.CurrencyGroupSizes = New Integer(1) { 2 } nfi.CurrencySymbol = "Dinheiro do Brasil " Dim d As Double = 8789282212.9384738747 Console.WriteLine(d.ToString("C", nfi)) C# NumberFormatInfo nfi = new NumberFormatInfo(); nfi.CurrencyDecimalDigits = 3; nfi.CurrencyDecimalSeparator = "*"; nfi.CurrencyGroupSeparator = "_"; nfi.CurrencyGroupSizes = new int[1] { 2 }; nfi.CurrencySymbol = "Dinheiro do Brasil "; Double d = 8789282212.9384738747; Console.WriteLine(d.ToString("C", nfi));

Page 217: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 13

Israel Aece | http://www.projetando.net

13

O resultado desse código é mostrado abaixo: Dinheiro do Brasil 87_89_28_22_12*938 Criando uma cultura customizada Como vimos até o momento, utilizamos as culturas definidas pelo próprio .NET Framework, como é o caso de pt-BR, en-US ou pt-PT. Essas culturas satisfazem a maior parte das aplicações que necessitam serem globalizadas. Só que pode haver cenário onde é necessário criarmos uma cultura própria, customizando-a para que atenda ao problema pelo qual ela foi criada. Felizmente, como o .NET Framework é estensível, ele fornece uma classe chamada CultureAndRegionInfoBuilder que permite a criação de uma cultura customizada de forma bem simples e rápida, sem a necessidade de aplicar o conceito de herança. Ela, por sua vez, contém várias propriedades e métodos que nos auxiliam na criação dessa cultura customizada e que é importante analisá-los para saber qual a sua finalidade: Membro Descrição CompareInfo Define um objeto do tipo CompareInfo que define como as

strings serão comparadas com essa cultura. CultureName Propriedade de somente leitura que retorna o nome da

cultura. GregorianDateTimeFormat Define um objeto do tipo DateTimeFormatInfo que define

como as datas e horas são tratadas com essa cultura. NumberFormat Define um objeto do tipo NumberFormatInfo que define

como os números (e valores monetários) são tratados com essa cultura.

LoadDataFromCultureInfo Método que recebe como parâmetro um objeto do tipo CultureInfo que carrega as propriedades do objeto corrente com as propriedades correspondentes da cultura informada.

LoadDataFromRegionInfo Método que recebe como parâmetro um objeto do tipo RegionInfo que carrega as propriedades do objeto corrente com as propriedades correspondentes da região informada

Register Persiste a cultura criada como uma cultura customizada no computador local e a disponibiliza para as aplicações.

Save Permite persistir a cultura criada em um arquivo físico, em formato XML para uso futuro.

Unregister Método estático que, dado uma string contendo o nome da cultura customizada, ele exclui a mesma do computador local.

Page 218: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 14

Israel Aece | http://www.projetando.net

14

Para exemplificar a criação desa cultura customizada, vamos analisar o código abaixo: VB.NET Dim cultureKey As String = "pt-BRCustom" Dim cst As Newew CultureAndRegionInfoBuilder( _ cultureKey, _ CultureAndRegionModifiers.None) cst.LoadDataFromCultureInfo(New CultureInfo("pt-BR")) cst.LoadDataFromRegionInfo(New RegionInfo("pt-BR")) cst.NumberFormat.CurrencyDecimalDigits = 4 cst.NumberFormat.CurrencyDecimalSeparator = "*" cst.Register() Dim pt As New CultureInfo(cultureKey) Dim valor As Double = 8789282212.9384738747 Console.WriteLine(valor.ToString("C", pt.NumberFormat)) CultureAndRegionInfoBuilder.Unregister(cultureKey) C# string cultureKey = "pt-BRCustom"; CultureAndRegionInfoBuilder cst = new CultureAndRegionInfoBuilder( cultureKey, CultureAndRegionModifiers.None); cst.LoadDataFromCultureInfo(new CultureInfo("pt-BR")); cst.LoadDataFromRegionInfo(new RegionInfo("pt-BR")); cst.NumberFormat.CurrencyDecimalDigits = 4; cst.NumberFormat.CurrencyDecimalSeparator = "*"; cst.Register(); CultureInfo pt = new CultureInfo(cultureKey); double valor = 8789282212.9384738747; Console.WriteLine(valor.ToString("C", pt.NumberFormat)); CultureAndRegionInfoBuilder.Unregister(cultureKey); O resultado desse código é mostrado abaixo: R$ 8.789.282.212*9385 Nota: Apesar da classe CultureAndRegionInfoBuilder estar contida dentro do namespace System.Globalization, é necessário adicionar uma referência ao Assembly sysglobl.dll na aplicação que desejar utilizá-la.

Page 219: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 15

Israel Aece | http://www.projetando.net

15

Encoding A codificação de caracteres é a forma que temos de representar caracteres em uma seqüencia de bits. Cada caracter, depois de codificado, é representado por um código único (também chamado de code point) dentro de uma code page. Uma code page é uma lista de code points em uma certa ordem e é utilizada para suportar idiomas específicos ou grupos de idiomas que compartilham o mesmo sistema de escrita. As code pages do Windows contém 256 code points (baseado em zero: 0 – 255). Na maioria das code pages, os primeiros 127 code points são sempre os mesmos caracteres para permitir a compatibilidade com o legado. Os 128 code points restantes diferem entre as code pages. Existem vários padrões de codificação disponíveis para representar os caracteres nos mais diversos idiomas. Inicialmente o padrão ASCII, que é baseado no alfabeto inglês foi desenvolvido pela IBM. Esse padrão utiliza 7 bits mas como há idiomas que possuem vários outros caracteres e, conseqüentemente, esse padrão não suportaria todos os idiomas. Neste momento é introduzido o padrão Unicode, que possibilita 8 bits para os caracteres. Além de suportar os caracteres definidos pelo ASCII, ele também suporta todos os caracteres conhecidos usados nos mais diversos idiomas. Atualmente, temos 3 “versões” do padrão Unicode: UTF-8, UTF-16 e UTF-32. O que diferem nestes padrões, é a quantidade de bytes utilizados para armazenar os caracteres: o primeiro utiliza 1 byte, o segundo 2 bytes e o último 4 bytes. O .NET Framework suporta esses padrões que podemos, através de classes, utilizá-los em nossas aplicações. As classes para isso estão contidas dentro do namespace System.Text e, uma das princpais delas é a classe Encoding. O .NET Framework fornece as seguintes implementações da classe Encoding para suportar alguns dos padrões existentes atualmente: Classe Descrição ASCIIEncoding Codifica os caracteres baseando-se no padrão ASCII. Essa classe

corresponde ao code page 20127. Para criá-la basta criar uma instância da mesma ou chamar a propriedade estática ASCII da classe Encoding que já retornará uma instância dessa classe.

UTF7Encoding Codifica os caracteres baseando-se no padrão UTF-7. Essa classe corresponde ao code page 65000. Para criá-la basta criar uma instância da mesma ou chamar a propriedade estática UTF7 da classe Encoding que já retornará uma instância dessa classe.

UTF8Encoding Codifica os caracteres baseando-se no padrão UTF-8. Essa classe corresponde ao code page 65001.

Page 220: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 16

Israel Aece | http://www.projetando.net

16

Para criá-la basta criar uma instância da mesma ou chamar a propriedade estática UTF8 da classe Encoding que já retornará uma instância dessa classe.

UnicodeEncoding Codifica os caracteres baseando-se no padrão UTF-16. Esse padrão podem utilizar uma espécie de ordenação (little-endian e big-endian), onde cada uma delas representam um code page diferente. A primeira deles equivale ao code page 1200 e a segunda ao code page 1201. Para criá-la basta criar uma instância da mesma ou chamar a propriedade estática Unicode da classe Encoding que já retornará uma instância dessa classe.

UTF32Encoding Codifica os caracteres baseando-se no padrão UTF-32. Esse padrão podem utilizar uma espécie de ordenação (little-endian e big-endian), onde cada uma delas representam um code page diferente. A primeira deles equivale ao code page 65005 e a segunda ao code page 65006. Para criá-la basta criar uma instância da mesma ou chamar a propriedade estática UTF32 da classe Encoding que já retornará uma instância dessa classe.

Nota: Para uma referência completa de todas as code pages e seus respectivos códigos, sugiro consultar a documentação da classe Encoding do MSDN Library. Além das propriedade estáticas que vimos acima que a classe Encoding fornece, ainda existem algumas outras propriedades e métodos importantes e que merecem serem citados. A tabela abaixo descreve alguns desses membros: Membro Descrição BodyName Propriedade de somente leitura que retorna uma string contendo o

nome do codificador corrente. CodePage Propriedade de somente leitura que retorna um número inteiro contendo

o identificar do codificar corrente. Default Propriedade estática de somente leitura que retorna um objeto do tipo

Encoding representando o codificador corrente do sistema. EncodingName Propriedade de somente leitura que retorna uma string contendo a

descrição (em forma legível) do codificador corrente. Convert Método estático que converte um array de bytes de um codificador para

outro. GetBytes Método que retorna um array de bytes contendo o resultado da

codificação dos caracteres passado para o método. GetDecoder Método que retorna um objeto do tipo Decoder, que é responsável por

converter uma determinada seqüencia de bytes em uma seqüencia de caracteres.

Page 221: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 17

Israel Aece | http://www.projetando.net

17

GetEncoder Método que retorna um objeto do tipo Encoder, que é responsável por converter uma determinada seqüencia de caracteres em uma seqüencia de bytes.

GetPreamble Método que retorna um array de bytes que irá identificar se o codificador suporta ordenação dos bytes em um formato big-endian ou little-endian.

GetString Método que, dado um array de bytes, ele decodifica e retorna uma string contendo o seu valor legível.

Para demonstrar a utilização de duas dessas classes de codificação fornecidas pelo .NET Framework (ASCIIEncoding e UnicodeEncoding), vamos analisar o código abaixo que, dado uma mensagem, ele recupera os bytes do mesma e, em seguida, traz em seu formato original. VB.NET Imports System.Text Sub Main() Dim msg As String = "Codgificação - .NET Framework" Dim ascii As New ASCIIEncoding Dim unicode As New UnicodeEncoding Dim asciiBytes() As Byte = ascii.GetBytes(msg) Dim unicodeBytes() As Byte = unicode.GetBytes(msg) ShowBytes(asciiBytes) ShowBytes(unicodeBytes) Dim asciiMsg As String = ascii.GetString(asciiBytes) Dim unicodeMsg As String = unicode.GetString(unicodeBytes) Console.WriteLine(Environment.NewLine) Console.WriteLine(asciiMsg) Console.WriteLine(unicodeMsg) End Sub Sub ShowBytes(ByVal msg() As Byte) Console.WriteLine(Environment.NewLine) For Each b As Byte In msg Console.Write("[{0}]", b) Next End Sub C# using System.Text; static void Main(string[] args)

Page 222: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 18

Israel Aece | http://www.projetando.net

18

{ string msg = "Codificação - .NET Framework"; ASCIIEncoding ascii = new ASCIIEncoding(); UnicodeEncoding unicode = new UnicodeEncoding(); byte[] asciiBytes = ascii.GetBytes(msg); byte[] unicodeBytes = unicode.GetBytes(msg); ShowBytes(asciiBytes); ShowBytes(unicodeBytes); string asciiMsg = ascii.GetString(asciiBytes); string unicodeMsg = unicode.GetString(unicodeBytes); Console.WriteLine(Environment.NewLine); Console.WriteLine(asciiMsg); Console.WriteLine(unicodeMsg); } static void ShowBytes(byte[] msg) { Console.WriteLine(Environment.NewLine); foreach (byte b in msg) { Console.Write("[{0}]", b); } } O resultado para ambos os código é: [67][111][100][103][105][102][105][99][97][63][63][111][32][45][32][46][78][69][ 84][32][70][114][97][109][101][119][111][114][107] [67][0][111][0][100][0][103][0][105][0][102][0][105][0][99][0][97][0][231][0][22 7][0][111][0][32][0][45][0][32][0][46][0][78][0][69][0][84][0][32][0][70][0][114 ][0][97][0][109][0][101][0][119][0][111][0][114][0][107][0] Codgifica??o - .NET Framework Codgificaçao - .NET Framework Como falamos mais acima, o padrão ASCII somente somente 256 caracteres e, somente caracteres do alfabeto inglês. Como o idioma inglês não possui caracteres acentuadas,

Page 223: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 19

Israel Aece | http://www.projetando.net

19

algumas coisas são perdidas e, quando não são encontradas um correspondente, um ponto de interrogação “?” é colocado no lugar do caracter desconhecido por aquele padrão. Tratando as falhas na codificação/decodificação Quando utilizamos algum codificador que não consegue codificar ou decodificar algum caracter, ele coloca um ponto de interrogação “?” para indicar que o codificador corrente não é capaz de “traduzí-lo”. A classe Encoding fornece dois métodos chamados GetEncoder e GetDecoder, que retornam os objetos responsáveis por codificar e decodificar as strings, respectivamente. Ambas as classes possuem uma propriedade chamada FallBack. Essa propriedade recebe um objeto do tipo EncoderFallback ou DecoderFallback que representam uma ação que será executada quando um caracter ou um byte não puder ser convertido. As classes EncoderFallback e DecoderFallback são utilizadas na codificação e decodificação, respectivamente. A primeira delas é a classe base para todos os fallbacks de codificação e, a segunda, a classe base para todos os fallbacks de decodificação. Conseguimos, através da imagem abaixo, entender a hierarquia dessas classes.

Page 224: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 20

Israel Aece | http://www.projetando.net

20

Imagem 7.1 – Hierarquia dos fallbacks de codificação e decodificação.

Basicamente, para ambos os casos, temos dois tipos de fallbacks:

• Fallbacks de Substituição: Quando um caracter ou byte não consegue ser “traduzido”, ele substitui o mesmo por um caracter que podemos determinar. Por padrão, o ponto de interrogação “?” é utilizado.

o EncoderReplacementFallback: Fallback que é invocado quando um caracter que puder ser convertido em uma seqüencia de bytes, convertendo o caracter para um valor pré-definido.

o DecoderReplacementFallback: Fallback que é invocado quando uma seqüencia de bytes não puder ser convertida em um caracter, convertendo o byte para um valor pré-definido.

• Fallbacks de Excessões: Quando um caracter ou byte não consegue ser “traduzido”, ele atira uma excessão do tipo EncoderFallbackException ou DecoderFallbackException, dependendo da operação que estamos tentando realizar.

Page 225: Livro de .NET - Israel Aece

Capítulo 7 – Globalização de Aplicações 21

Israel Aece | http://www.projetando.net

21

o EncoderExceptionFallback: Fallback que é invocado quando um caracter que puder ser convertido em uma seqüencia de bytes, atirando uma excessão do tipo EncoderFallbackException.

o DecoderExceptionFallback: Fallback que é invocado quando uma seqüencia de bytes não puder ser convertida em um caracter, atirando uma excessão do tipo DecoderFallbackException.

O código abaixo exemplifica a utilização dos fallbacks: VB.NET Imports System.Text Dim encoder As Encoder = Encoding.ASCII.GetEncoder() encoder.Fallback = New EncoderReplacementFallback("*") Dim chars() As Char = "ãBC".ToCharArray() Dim buffer(chars.Length) As Byte encoder.GetBytes(chars, 0, chars.Length, buffer, 0, True) Console.WriteLine(Encoding.ASCII.GetString(buffer)) C# using System.Text; Encoder encoder = Encoding.ASCII.GetEncoder(); encoder.Fallback = new EncoderReplacementFallback("*"); char[] chars = "ãBC".ToCharArray(); Byte[] buffer = new byte[chars.Length]; encoder.GetBytes(chars, 0, chars.Length, buffer, 0, true); Console.WriteLine(Encoding.ASCII.GetString(buffer)); O resultado para ambos os código é: *BC O "ã" é substituído pelo "*" porque esse caracter não está contemplado no padrão ASCII e, conseqüentemente, não consegue ser "traduzido". A utilização dos fallbacks são muito úteis quando você quer customizar a leitura ou gravação de streams. A utilização de fallbacks de excessões são as vezes mais utilizadas quando a leitura dos caracteres devem ser muito mais precisa.

Page 226: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 1

Israel Aece | http://www.projetando.net

1

Capítulo 8 Criptografia Introdução Criptografia de dados é um ponto muito importante nos mais diversos tipos de aplicações. Geralmente, em aplicações onde alguns dos dados são muito sigilosos, como é o caso de aplicações financeiras, quais mantém os dados de seus clientes, é necessário que se mantenha esses dados seguros e desejavelmente, se esses dados cairem em mãos erradas, essas pessoas com más intenções, não consigam entender e recuperar esses dados em sua forma legível. É justamente neste cenário que a criptografia entra, ou seja, ao se detectar os dados sensíveis durante a análise do projeto, deve ser aplicar algum algoritmo de criptografia para manter esses dados o mais seguro possível. Isso é vital para uma aplicação deste nível. Neste capítulo analisaremos as possibilidades que o .NET Framework nos fornece para aplicar criptografia em dados, bem como executar o processo reverso, ou seja, ser capaz de trazer os dados em sua forma legível. Todos os recursos estão contidos dentro do namespace System.Security.Cryptography. Ainda há a possibilidade de efetuarmos a criptografia em uma única direção, ou seja, não sermos mais capazes de recuperar o seu conteúdo em sua forma legível. Esse processo é conhecido como hash e será extensivamente abordado na segunda parte deste capítulo. O que é criptografia? Criptografia trata-se de o processo de converter alguns dados em um formato difícil de ler e compreender. O processo de criptografia é capaz de, dado um conteúdo qualquer (número, letras, etc.), transformá-lo em uma seqüencia de caracteres que, a olha humano, não é capaz de ser traduzido. A criptografia é utilizada para:

1. Confidencialidade: Para garantir que os dados permaneçam privados. Geralmente, a confidencialidade é obtida com a criptografia. Os algoritmos de criptografia (que usam chaves de criptografia) são usados para converter texto sem formatação em texto codificado e o algoritmo de descriptografia equivalente é usado para converter o texto codificado em texto sem formatação novamente. Os algoritmos de criptografia simétricos usam a mesma chave para a criptografia e a descriptografia, enquanto que os algoritmos assimétricos usam um par de chaves pública/privada.

2. Integridade de dados: Para garantir que os dados sejam protegidos contra modificação acidental ou deliberada (mal-intencionada). A integridade, geralmente, é fornecida por códigos de autenticação de mensagem ou hashes. Um valor de hash é um valor numérico de comprimento fixo derivado de uma seqüência de dados. Os valores de hash são usados para verificar a integridade

Page 227: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 2

Israel Aece | http://www.projetando.net

2

dos dados enviados por canais não seguros. O valor do hash de dados recebidos é comparado ao valor do hash dos dados, conforme eles foram enviados para determinar se foram alterados.

3. Autenticação: Para garantir que os dados se originem de uma parte específica. Os certificados digitais são usados para fornecer autenticação. As assinaturas digitais geralmente são aplicadas a valores de hash, uma vez que eles são significativamente menores que os dados de origem que representam.

Os algoritmos de criptografia utilizam um valor um tanto quanto complexo, chamado de cipher (ou como key), para ser utilizado no processo de criptografia. O processo que utiliza o cipher para encriptar os dados e produzir o valor já criptografado é chamado de algoritmo de criptografia. Os valores que compõem os cipher podem ser de diferentes tamanhos e, quanto maior, mais seguro é e, conseqüentemente, mais difícil de ser “quebrado”. Como já dissemos na introdução do capítulo, o processo de criptografia é um processo que consiste em duas rotinas: criptografar os dados e decriptografá-los, que é justamente o processo inverso, ou seja, dado o valor criptografado, ele é capaz de recuperar o seu conteúdo em sua legível. Mas é importante lembrar que esse processo somente é possível se a chave utilizada for a mesma chave (cipher) aplicada durante o processo de criptografia. Dentro do processo de criptografia, existem duas categorias de algoritmos. Essas categorias são baseadas em que cipher é utilizado e como ele é gerenciado. Essas categorias são chamadas de criptografia simétrica e criptografia assimétrica. A primeira delas, a criptografia simétrica, utiliza uma chave única privada, tanto para criptografar quanto para descriptografar os dados. Já a criptografia assimétrica, utiliza um par de chaves, sendo uma pública e uma privada. Uma delas é mantida privada e a outra é ditribuída publicamente. Criptografia simétrica A criptografia simétrica utiliza apenas uma chave privada que é necessária tanto para criptografar quanto para descriptografar as informações e é utilizada em um grupo pequenos de parceiros e companhias. Os algoritmos de criptografia simétricas são menos complexos e mais eficiente que os algoritmos de criprografia assimétricas, justamente porque possuem apenas uma única chave. Sendo assim, se essa chave cair nas mãos de pessoas erradas, poderá comprometer a segurança dos dados, já que ela poderá descriptografar os dados sem nenhum outro problema e, se isso realmente acontecer, você deve obrigatoriamente e o mais rápido possível trocar essa chave. Devido a essa enorme responsabilidade de ter que manter essa chave segura, muitas empresas optam por utilizar a criptografia assimétrica, que veremos mais adiante.

Page 228: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 3

Israel Aece | http://www.projetando.net

3

Dentro do .NET Framework temos os mais conhecidos algoritmos de criptografia simétrica implementadas em diversas classes. Essas classes estão contidas dentro do namespace System.Security.Cryptography. Entre os algoritmos temos: Data Encryption Standard (DES), Triple DES, RC2 (Rivest Cipher), e Rijndael. O imagem abaixo ilustra a hierarquia das classes que fornecem os mais algoritmos simétricos:

Imagem 8.1 – Hierarquia das classes de criptografia simétrica

Como podemos notar, todas as classes desta categoria de criptografia herdam diretamente da classe abstrata SymmetricAlgorithm, que fornece todas as funcionalidades básicas para qualquer algoritmo de criptografia simétrica. Todas as classes que herdam dela, como é o caso das classes DES, TripleDES, RC2 e Rijndael, são também definidas como abstratas e, mais tarde, serão implementadas pela classe concreta, que veremos a seguir. Cada uma dessas classes que herdam diretamente da classe abstrata SymmetricAlgorithm, são agora herdadas, criando um CSP (Cryptographic Service Provider) correspondente para cada um dos algoritmos. Um CSP é um módulo independente que atualmente executa os algoritmos de criptografia para autenticação, codificação e criptografia, ou seja, é basicamente um wrapper para o algoritmo de criptografia correspondente, encapsulando o acesso a objetos não gerenciados pelo runtime do .NET Framework que são utilizados durante o processo de criptografia. Para termos uma noção melhor do que cada uma das classes fornecem, vamos analisar a relação abaixo: Algoritmo Descrição DES Criado pela IBM em 1975, suporta uma chave de 56 bits, mas dentro do

.NET Framework, possibilita o uso de uma chave de 64 bits. O .NET Framework suporta apenas porque esse algoritmo é popularmente utilizado, mas já foi “quebrado”. Essa classe é base para todos os algoritmos baseando em DES e sua CSP correspondente é a classe

Page 229: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 4

Israel Aece | http://www.projetando.net

4

DESCryptoServiceProvider. Essa classe ainda possui dois métodos públicos e estáticos importantes, que analisam a complexidade e determinam se a chave é ou não fácil de ser “quebrada”. Esses métodos são invocados durante o proceso de criptografia e descriptografia e atira uma exceção do tipo CryptographicException caso a chave seja fácil de ser “quebrada”, o que obriga-nos a chamar os métodos dentro de um bloco de tratamento de erros para antecipar esses possíveis problemas. Os métodos são:

• IsWeakKey: Existem quatro chaves que são consideradas fracas e facilmente de serem quebradas. Esse método retorna um valor booleano indicando se a chave fornecida é ou não fraca.

• IsSemiWeakKey: Existe seis chaves que são consideradas semi-fracas e podem ser facilmente quebradas. Esse método retorna um valor booleano indicando se a chave fornecida é ou não semi-fraca.

Nota: Felizmente o método GenerateKey nunca retornará uma chave que é fraca ou semi-fraca.

TripleDES Criado pela IBM em 1978, suporta uma chave de 168 bits, mas dentro do .NET Framework, possibilita o uso de uma chave de 128 até 192 bits. Essa classe é base para todos os algoritmos baseando em TripleDES e sua CSP correspondente é a classe TripleDESCryptoServiceProvider. Esse algoritmo é baseado no algoritmo DES e, sendo assim, contém também chaves conhecidas que podem quebrar a criptografia. No entanto, esse algoritmo é considerado muito mais seguro em comparação ao DES, justamente por ter uma chave maior e o algoritmo é aplicado três vezes antes de disponibilizar o resultado. Essa classe também possui um método público e estático chamado IsWeakKey que retorna um valor booleano indicando se a chave fornecida é ou não fraca.

RC2 Representa a classe base para todas as implementações do algoritmo RC2. Esse algoritmo foi criado em 1987 por Ron Rivest. Esse algoritmo suporta chaves de 40 até 128 bits. Essa classe é base para todos os algoritmos baseando em RC2 e sua CSP correspondente é a classe RC2CryptoServiceProvider.

Rijndael Criado em 1998 por Joan Daemen e Vincent Rijmen, Rijndael também é conhecida como Advanced Encryption Standard (AES). Essa classe suporta chaves de 128 até 256 bits e, dentro do .NET Framework, o algoritmo Rijndael suporta valores fixos de 128, 192 ou 256 bits. Essa classe é base para todos os algoritmos baseando em Rijndael e a classe concreta que implementa essa algoritmo dentro do .NET Framework é a

Page 230: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 5

Israel Aece | http://www.projetando.net

5

classe RijndaelManaged. Tipos adicionais Antes de analisarmos exemplos de códigos que utilizamos para efetuarmos os processos de criptografia e descriptografia, devemos estudar um pouco mais sobre alguns tipos adicionais que temos dentro do .NET Framework, que são utilizados em conjunto com as classes responsáveis pela criptografia em si. O primeiro deles é a classe CryptoStream. Essa classe herda diretamente da classe Stream e fornece um link entre o stream de dados e as transformações realizadas durante o processo de criptografia. Seu construtor recebe três parâmetros: um objeto do tipo Stream, que é onde os dados criptografados serão armazenados; já o segundo parâmetro, recebe um objeto do tipo ICryptoTransform. Essa Interface é implementada em classes que define as operações básicas de transformações criptográficas. Instâncias de objetos que implementam esta Interface são retornados quando invocamos os métodos CreateEncryptor e CreateDecryptor referente à algum algoritmo fornecido pela plataforma .NET. Finalmente, o terceiro e último parâmetro, recebe uma das opções fornecidas pelo enumerador CryptoStreamMode, que irá indicar qual será o modo que o stream será operado. As opções que temos neste enumerador são: Opção Descrição Read Permite acesso à leitura ao stream de criptografia. Write Permite acesso à escrita ao stream de criptografia. Além da classe CryptoStream ainda fazemos uso de algumas classes contidas dentro do namespace System.IO para poder efetuar a leitura e a escrita dos dados criptografados. Entre essas classes temos a classe MemoryStream, StreamWriter e StreamReader, quais já abordamos anteriormente no Capítulo 5. No cenário de criptografia, a classe MemoryStream é utilizada para armazenar os dos que serão criptografados ou descriptografados; já as classes StreamWriter e StreamReader são utilizadas no processo de criptografia e descriptografia de dados, respectivamente. Por questões de espaço, não abordaremos todos os algoritmos de criptografia simétrica aqui. Apenas como exemplo, teremos a implementação aqui do algoritmo DES e na aplicação de demonstração do capítulo teremos todos os algoritmos implementados. Sendo assim, o exemplo abaixo utiliza um único CSP para efetuar tanto a criptografia quanto a descriptografia de uma mensagem simples. VB.NET Imports System Imports System.IO Imports System.Text Imports System.Security.Cryptography

Page 231: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 6

Israel Aece | http://www.projetando.net

6

Using csp As New DESCryptoServiceProvider() Dim buffer() As Byte = Nothing Using ms As New MemoryStream() Using stream As New CryptoStream(ms, csp.CreateEncryptor, CryptoStreamMode.Write) Using sw As New StreamWriter(stream) sw.Write("Trabalhando com Criptografia") End Using End Using buffer = ms.ToArray() Console.WriteLine(Encoding.Default.GetString(buffer)) End Using Using ms As New MemoryStream(buffer) Using stream As New CryptoStream(ms, csp.CreateDecryptor(), CryptoStreamMode.Read) Using sr As New StreamReader(stream) Console.WriteLine(sr.ReadLine()) End Using End Using End Using End Using C# using System; using System.IO; using System.Text; using System.Security.Cryptography; using (DESCryptoServiceProvider csp = new DESCryptoServiceProvider()) { byte[] buffer = null; using (MemoryStream ms = new MemoryStream()) { using (CryptoStream stream = new CryptoStream(ms, csp.CreateEncryptor(), CryptoStreamMode.Write)) { using (StreamWriter sw = new StreamWriter(stream)) { sw.WriteLine("Trabalhando com Criptografia"); } } buffer = ms.ToArray(); Console.WriteLine(Encoding.Default.GetString(buffer));

Page 232: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 7

Israel Aece | http://www.projetando.net

7

} using (MemoryStream ms = new MemoryStream(buffer)) { using (CryptoStream stream = new CryptoStream(ms, csp.CreateDecryptor(), CryptoStreamMode.Read)) { using (StreamReader sr = new StreamReader(stream)) { Console.WriteLine(sr.ReadLine()); } } } } Analisando o código acima e como já foi dito, utilizamos um único CSP do tipo DESCryptoServiceProvider que é utilizado tanto para criptografar quanto para descriptografar a mensagem. O método CreateEncryptor e CreateDecryptor são sobrecarregados; para cada um deles existe a possibilidade de invocá-los sem a necessidade de parâmetros e, neste caso, os valores da chave (propriedade Key) e do vetor (propriedade IV) são passados automaticamente e, se esses valores não foram definidos pelo desenvolvedor, como foi o caso acima, os métodos GenerateKey e GenerateIV são invocados. Nota: IV (Initialization Vector) trata-se de uma valor que é aplicado na criptografia simétrica para garantir que uma mesma mensagem não seja criptografada da mesma forma, o que poderia corromper o algoritmo, ou seja, sabendo que a palavra “Microsoft” fosse sempre criptografada com um determinado conjunto de caracteres, bastaria percorrer o restante da mensagem e onde fosse encontrado esse conjunto, sabe-se que é a palavra “Microsoft” que ali está. Como estamos fazendo os dois processos de uma única vez, não precisamos nos preocupar com a chave e o vetor que são automaticamente gerados, justamente porque utilizamos a mesma instância do CSP DESCryptoServiceProvider. Obviamente que esses valores devem ser guardados com muita segurança se futuramente você desejar descriptografar a mensagem. Lembrando que esses valores também devem ser distribuídos para todos que podem descriptografar os dados. Criptografia Assimétrica Também conhecida como “chave pública”, a chave assimétrica trabalha com duas chaves: uma denominada privada e a outra pública. Nesse método, uma pessoa deve criar uma chave de codificação e enviá-la a quem for mandar informações a ela. Essa é a chave pública. Uma outra chave deve ser criada para a decodificação. Esta chave, também conhecida como chave privada, é secreta e não deve ser compartilhada.

Page 233: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 8

Israel Aece | http://www.projetando.net

8

Criptografia assimétrica é utilizada em larga escala, pois é ideal para cenários como por exemplo a própria Internet, onde a chave privada permanece secreta e a chave pública será largamente distribuída. Um outro exemplo típico de criptografia assimétrica é utilizada em assinaturas digitais, que é muito usado com chaves públicas. Trata-se de um meio que permite provar que um determinado documento eletrônico é de procedência verdadeira. O receptor da informação usará a chave pública fornecida pelo emissor para se certificar da origem. Além disso, a chave fica integrada ao documento de forma que qualquer alteração por terceiros imediatamente a invalide. O .NET Framework utiliza essa técnica quando “assinamos” um Assembly com um strong name. Como algoritmos de criptografia assimétrica são considerados mais complexos em relação aos algoritmos de criptografia simétrica, a criptografia assimétrica será executada com mais lentidão. Dentro do .NET Framework temos os mais conhecidos algoritmos de criptografia assimétrica implementadas em diversas classes. Essas classes estão contidas dentro do namespace System.Security.Cryptography. Entre os algoritmos temos o DSA e o RSA. O imagem abaixo ilustra a hierarquia das classes que fornecem os mais algoritmos assimétricos:

Imagem 8.2 – Hierarquia das classes de criptografia assimétrica

Como podemos notar, todas as classes desta categoria de criptografia herdam diretamente da classe abstrata AsymmetricAlgorithm, que fornece todas as funcionalidades básicas para qualquer algoritmo de criptografia simétrica. Todas as classes que herdam dela,

Page 234: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 9

Israel Aece | http://www.projetando.net

9

como é o caso das classes DSA e RSA, são também definidas como abstratas e, mais tarde, serão implementadas pela classe concreta, que veremos a seguir. Cada uma dessas classes que herdam diretamente da classe abstrata AsymmetricAlgorithm, são agora herdadas, criando um CSP (Cryptographic Service Provider) correspondente para cada um dos algoritmos. Um CSP é um módulo independente que atualmente executa os algoritmos de criptografia para autenticação, codificação e criptografia, ou seja, é basicamente um wrapper para o algoritmo de criptografia correspondente. Para termos uma noção melhor do que cada uma das classes fornecem, vamos analisar a relação abaixo: Algoritmo Descrição DSA Criado em 1991, o DSA (Digital Signature Algorithm) utiliza uma

chave de 512 até 1024 bits e é utilizada como base para todos os algoritmos de assinatura digital. Essa classe é base para todos os algoritmos baseando em DSA e sua CSP correspondente é a classe DSACryptoServiceProvider.

RSA Criado em 1977 por Ron Rivest, Adi Shamir e Len Adleman é um dos algoritmos mais utilizados atualmente. Dentro do .NET Framework é suportado uma chave de 384 até 16.384 bits se utilizar o Microsoft Enhanced Cryptographic Provider e uma chave de 384 até 512 bits se utilizar o Microsoft Base Cryptographic Provider. Essa classe é base para todos os algoritmos baseando em RSA e sua CSP correspondente é a classe RSACryptoServiceProvider.

DSA Como já vimos, o DSA é um algoritmo assimétrico e a sua chave privada opera sobre o hash da mensagem SHA-1. Para verificar a assinatura um pedaço do código calcula o hash e outro pedaço usa a chave pública para decifrar a assinatura, e por fim ambos comparam os resultados garantindo a autoria da mensagem. O DSA trabalha com chaves de 512 até 1024 bits, porém ao contrário do RSA, o DSA somente assina e não garante confidencialidade. Outro ponto contra o DSA é que a geração da assinatura é mais rápida do que o RSA, porém de 10 até 40 vezes mais lenta para conferir a assinatura. Para exemplificar a utilização deste algoritmo, iremos utilizar a classe DSACryptoServiceProvider fornecida pelo .NET Framework. Além dessa classe, ainda temos outras duas classes que devemos utilizar: DSASignatureFormatter e DSASignatureDeformatter. A primeira delas é utilizada para criar um algoritmo de assinatura digital através de um método chamado CreateSignature; já a segunda, é utilizada para verificar se uma determinada assinatura é ou não válida, também através de um método chamado VerifySignature. O código mostra como fazermos para utilizar as classes acima mencionadas:

Page 235: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 10

Israel Aece | http://www.projetando.net

10

VB.NET Imports System Imports System.Security.Cryptography Dim hash As Byte() = {22, 45, 78, 53, 1, 2, 205, 98, 75, 123, 45, 76, 143, 189, 205, 65, 12, 193, 211, 255} Dim algoritmo As String = "SHA1" Using csp As New DSACryptoServiceProvider Dim formatter As New DSASignatureFormatter(csp) formatter.SetHashAlgorithm(algoritmo) Dim signedHash As Byte() = formatter.CreateSignature(hash) Dim deformatter As New DSASignatureDeformatter(csp) deformatter.SetHashAlgorithm(algoritmo) If deformatter.VerifySignature(hash, signedHash) Then Console.WriteLine("Assinatura válida.") Else Console.WriteLine("Assinatura inválida.") End If End Using C# using System; using System.Security.Cryptography; byte[] hash = { 22, 45, 78, 53, 1, 2, 205, 98, 75, 123, 45, 76, 143, 189, 205, 65, 12, 193, 211, 255 }; string algoritmo = "SHA1"; using (DSACryptoServiceProvider csp = new DSACryptoServiceProvider()) { DSASignatureFormatter formatter = new DSASignatureFormatter(csp); formatter.SetHashAlgorithm(algoritmo); byte[] signedHash = formatter.CreateSignature(hash); DSASignatureDeformatter deformatter = new DSASignatureDeformatter(csp); deformatter.SetHashAlgorithm(algoritmo); if (deformatter.VerifySignature(hash, signedHash)) { Console.WriteLine("Assinatura válida."); } else

Page 236: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 11

Israel Aece | http://www.projetando.net

11

{ Console.WriteLine("Assinatura inválida."); } } RSA O exemplo do algoritmo RSA é mais simples em relação ao algoritmo DSA. Dentro do .NET Framework temos a classe (CSP) RSACryptoServiceProvider que fornece uma implementação do algoritmo RSA. Essa classe fornece dois métodos chamados Encrypt e Decrypt. Ambos recebem um array de bytes onde, no primeiro caso, o método Encrypt, o array de bytes representa a mensagem a ser criptografada e retornará um array de bytes com a mensagem criptografada; já o segundo método, Decrypt, receberá um array de bytes contendo a mensagem criptografada e retornará um array de bytes com a mensagem descriptografada. O exemplo abaixo exibe a utilização da implementação do algoritmo RSA fornecido pelo .NET Framework: VB.NET Imports System Imports System.Security.Cryptography Using csp As New RSACryptoServiceProvider Dim msg As Byte() = Encoding.Default.GetBytes("Trabalhando com criptografia") Dim msgEncriptada As Byte() = csp.Encrypt(msg, True) Console.WriteLine(Encoding.Default.GetString(msgEncriptada)) Dim msgDescriptada As Byte() = csp.Decrypt(msgEncriptada, True) Console.WriteLine(Encoding.Default.GetString(msgDescriptada)) End Using C# using System; using System.Security.Cryptography; using (RSACryptoServiceProvider csp = new RSACryptoServiceProvider()) { byte[] msg = Encoding.Default.GetBytes("Trabalhando com criptografia"); byte[] msgEncriptada = csp.Encrypt(msg, true); Console.WriteLine(Encoding.Default.GetString(msgEncriptada));

Page 237: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 12

Israel Aece | http://www.projetando.net

12

byte[] msgDescriptada = csp.Decrypt(msgEncriptada, true); Console.WriteLine(Encoding.Default.GetString(msgDescriptada)); } CryptoConfig Essa classe é utilizada em aplicações que não podem (ou não querem) depender de um algoritmo específico de criptografia ou hashing. Isso pode aumentar a segurança, já que a aplicação poderá ser flexível ao ponto de escolher um entre uma porção de algoritmos disponíveis e aplicar a criptografia/hashing. Pode-se optar por armazenar o algoritmo utilizado dentro da base de dados e, a qualquer momento que desejar, pode recuperar tanto o valor criptografado quanto o algoritmo que deve ser aplicado para descriptografar o valor. Essa classe possui um método chamado estático CreateFromName que recebe como parâmetro uma string contendo o algoritmo concreto que deseja criar, retornando um System.Object contendo a classe devidamente instanciada. Esse método ainda possui um overload que recebe um array de System.Object que são os parâmetros que devem ser passados para o construtor da classe, quando houver. A string que será informada para o método deve estar dentro de uma das opções da tabela abaixo: Nome/Chave Algoritmo que será instanciado SHA SHA1CryptoServiceProvider SHA1 SHA1CryptoServiceProvider System.Security.Cryptography.SHA1 SHA1CryptoServiceProvider System.Security.Cryptography.HashAlgorithm SHA1CryptoServiceProvider MD5 MD5CryptoServiceProvider System.Security.Cryptography.MD5 MD5CryptoServiceProvider SHA256 SHA256Managed SHA-256 SHA256Managed System.Security.Cryptography.SHA256 SHA256Managed SHA384 SHA384Managed SHA-384 SHA384Managed System.Security.Cryptography.SHA384 SHA384Managed SHA512 SHA512Managed SHA-512 SHA512Managed System.Security.Cryptography.SHA512 SHA512Managed RSA RSACryptoServiceProvider System.Security.Cryptography.RSA RSACryptoServiceProvider System.Security.Cryptography.AsymmetricAlgorithm

RSACryptoServiceProvider

DSA DSACryptoServiceProvider System.Security.Cryptography.DSA DSACryptoServiceProvider

Page 238: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 13

Israel Aece | http://www.projetando.net

13

DES DESCryptoServiceProvider System.Security.Cryptography.DES DESCryptoServiceProvider 3DES TripleDESCryptoServiceProvider TripleDES TripleDESCryptoServiceProvider Triple DES TripleDESCryptoServiceProvider System.Security.Cryptography.TripleDES TripleDESCryptoServiceProvider System.Security.Cryptography.SymmetricAlgorithm TripleDESCryptoServiceProvider RC2 RC2CryptoServiceProvider System.Security.Cryptography.RC2 RC2CryptoServiceProvider Rijndael RijndaelManaged System.Security.Cryptography.Rijndael RijndaelManaged O código abaixo exibe uma forma de utilizar essa classe, passando como parâmetro para o método CreateFromName o valor “SHA”, que fará com que uma instância do algoritmo do tipo SHA1CryptoServiceProvider seja retornado. VB.NET Imports System Imports System.Security.Cryptography Dim csp As SHA1CryptoServiceProvider = DirectCast(CryptoConfig.CreateFromName("SHA"), SHA1CryptoServiceProvider) C# using System; using System.Security.Cryptography; SHA1CryptoServiceProvider csp = (SHA1CryptoServiceProvider)CryptoConfig.CreateFromName("SHA"); O que é hashing? Ao contrário dos tipos de criptografias que vimos até o momento, o objetivo do hash não é garantir a confidenciabilidade de uma determinada informação, mas sim garantir que a mesma não sofra nenhuma espécie de adulteração. Sendo assim, a técnica de hashing é considera unidirecional, ou seja, uma vez aplicado o algoritmo de hashing em uma informação, jamais será possível recuperar aquela informação em seu estado legível. Isso quer dizer que um determinado valor sempre gerará o mesmo código hash e, para comparar se os dois valores hashes são iguais, você deve aplicar o algoritmo hash ao valor informado pelo usuário e, o valor gerado, deve ser comparado quanto à igualdade ao valor hash que tem armazenado dentro do sistema.

Page 239: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 14

Israel Aece | http://www.projetando.net

14

Obviamente que o mesmo algoritmo deve ser aplicado para garantir que seja possível efetuar a comparação. O .NET Framework suporte três algoritmos de hash, a saber: MD5, SHA1e HMAC e estão assim distribuídas:

Imagem 8.3 – Hierarquia das classes de hashing

Todos os algoritmos que implementam a técnica de hashing herdam, direta ou indiretamente, de uma classe abstrata chamada HashAlgorithm. Como podemos notar na imagem 8.3, essa classe abstrata é herdada diretamente pelas classes SHA1, MD5, KeyedHashAlgorithm, RIPEMD160, SHA256, SHA384 e SHA512. Cada uma dessas classes possuem características que diferem uma das outras. Essas características consistem basicamente no número de bits que cada uma operar e todas elas tratam-se de classes abstratas que possuem a implementação básica específica para cada algoritmo e devem obrigatoriamente devem serem implementadas pelas classes concretas. Para entendermos um pouco melhor sobre cada algorimo, vamos analisar a tabela abaixo: Algoritmo Descrição MD5 O MD5 (Message-Digest algorithm 5) é um algoritmo de hash de 128

bits unidirecional desenvolvido pela RSA Data Security, Inc., é o sucessor do MD4.

SHA1 A família SHA (Secure Hash Algorithm) é um sistema de funções criptográficas de hash. O primeiro membro da familia SHA foi publicado em 1993 e foi chamado de SHA. Entretanto atualmente foi deonominado por SHA-0 para evitar confusão com os seus sucesores. Dois anos mais tarde, o primeiro sucessor do SHA-0 foi publicado com o nome de SHA-1. Existem atualmente quatro variações deste algoritmo, que se difrerenciam nas interações e na sua saída, que foram melhorados. São eles: : SHA-224, SHA-256, SHA-384, e SHA-512 (Todos eles são referenciados como SHA-2). Já existem registros de ataques ao SHA-0 e ao SHA-1, mas nenhum registro ao SHA-2.

HMAC HMAC (Hash-based Message Authentication Code) é ma técnica que utiliza uma função de hash criptográfica, uma mensagem e uma chave. Constitui um dos meios predominantes para garantir que os dados não foram corrompidos durante a transição da mensagem entre o emissor e o

Page 240: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 15

Israel Aece | http://www.projetando.net

15

receptor. Qualquer algoritmo de hashing pode ser utilizado com HMAC, sendo preferíveis as funções mais seguras, como por exemplo a SHA-1.

Abaixo será exibido um exemplo para cada algoritmo que vimos na tabela acima. Basicamente, todas as classes que fornecem implementações de algoritmos de hashing, possuem um método chamado ComputeHash que recebem um array de bytes contendo a mensagem a ser aplicado o hash, devolvendo também um array de bytes, só que contendo o resultado do hash. Vamos iniciar pelo algortimo MD5. Assim como as classes de criptografias simétricas e assimétricas, as classes de hashing também possuem seus CSPs correspondentes e, no caso do algoritmo MD5, temos uma classe abstrata chamada MD5, que é a classe base para todos os algoritmos MD5 e a classe MD5CryptoServiceProvider que é o CSP correspondente. Abaixo analisaremos o código responsável por aplicar um algoritmo hash MD5: VB.NET Imports System Imports System.Security.Cryptography Using csp As New MD5CryptoServiceProvider Dim msg As Byte() = Encoding.Default.GetBytes("MinhaSenha") Dim hash As Byte() = csp.ComputeHash(msg) For i As Integer = 0 To hash.Length – 1 Console.Write(hash(i).ToString("x2")) Next End Using C# using System; using System.Security.Cryptography; using (MD5CryptoServiceProvider csp = new MD5CryptoServiceProvider()) { byte[] msg = Encoding.Default.GetBytes("MinhaSenha"); byte[] hash = csp.ComputeHash(msg); for (int i = 0; i < hash.Length; i++) Console.Write(hash[i].ToString("x2")); }

Page 241: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 16

Israel Aece | http://www.projetando.net

16

Como falamos acima, invocamos o método ComputeHash passando o valor onde será aplicado o hash e é devolvido um array de bytes contendo o valor do hash gerado. Apenas fazemos um laço For para converter cada byte em valor hexadecimal (isso é determinado pela sobrecarga do método ToString) correspondente para visualizarmos a valor, que é “775b70588a139c914412b7fdbc20d1c7” e este valor será sempre o mesmo para “MinhaSenha”. Utilizando agora o algoritmo SHA1, o código muda ligeiramente. Apenas o que irá mudar será o CSP utilizado. Só que para este algoritmo, temos dois CSPs disponíveis que são SDA1CryptoServiceProvider e o SHA1Managed. O resultado de ambos são idênticos, mas o que muda é seu comportamento interno, ou seja, a classe SHA1Managed é uma versão gerenciada do algoritmo SHA1, o que proporciona um gerenciamento melhor de memória, sem a necessidade de servir de wrapper para objetos não gerenciados, como é o caso do SDA1CryptoServiceProvider. Um exemplo da sua utilização é a seguinte: VB.NET Imports System Imports System.Security.Cryptography Using alg As New SHA1Managed Dim msg As Byte() = Encoding.Default.GetBytes("MinhaSenha") Dim hash As Byte() = alg.ComputeHash(msg) For i As Integer = 0 To hash.Length – 1 Console.Write(hash(i).ToString("x2")) Next End Using C# using System; using System.Security.Cryptography; using (SHA1Managed alg = new SHA1Managed()) { byte[] msg = Encoding.Default.GetBytes("MinhaSenha"); byte[] hash = alg.ComputeHash(msg); for (int i = 0; i < hash.Length; i++) Console.Write(hash[i].ToString("x2")); } O valor do hash gerado para a string “MinhaSenha” através da classe SHA1Managed é a seguinte: “16c5e5376a4654937efedc675e941e32f917985e”. Um detalhe importante aqui é que, se utilizarmos o CSP SHA1CryptoServiceProvider o resultado seria o mesmo.

Page 242: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 17

Israel Aece | http://www.projetando.net

17

Finalmente, iremos utilizar o algoritmo HMAC. Através da imagem 8.3, a classe abstrata HMAC é a classe base para todas as implementações deste algoritmo e estende a classe KeyedHashAlgorithm. Contrariamente às assinaturas digitais, os MAC’s são computados e verificados com a mesma chave, para que assim, possam ser apenas verificadas pelo respectivo receptor. Um cenário de exemplo é: A Empresa 1 (emissora da mensagem) e a Empresa 2 (receptora) compartilham uma mesma chave secreta. A Empresa 1 utiliza a mensagem e a chave para computar o MAC, e envia o MAC juntamente com a mensagem. Quando a Empresa 2 receber a mensagem, descodifica o MAC e verifica-o para ver se o seu MAC corresponde ao da Empresa 1. Se corresponder, então ele sabe que a mensagem é da Empresa 1 e que ninguém a modificou desde que foi enviada pela Empresa 1. Se alguém tentar corromper a mensagem, pode fazê-lo, mesmo não possuindo a chave secreta, contudo não consegue produzir o MAC. Neste caso, o receptor irá detectar a alteração que foi feita e sabe que a mensagem foi corrompida. Os algoritmos HMAC utilizam uma chave gerada randomicamente para aplicar o hash. Essa chave é bem semelhante ao Salt e é concatenada com o valor de hash gerado da mensagem a ser enviada. O resultado da concatenação entre a chave e o hash da mensagem é novamente submetido a ao algoritmo de hash, resultando em um valor duas vezes “hasheado” com um Salt. Como a chave é gerada randomicamente, a cada geração, o output para uma mesma mensamgem é completamente diferente. Nota: Salt são utilizados para dificultar o processo de hashing, ou seja, são valores adicionais, geralmente gerados randomicamente, que são concatenados na mensagem que irá sofrer o processo de hashing. Quando você manipula dados que estão sendo armazenados dentro de um banco de dados qualquer em seu formato “hashed”, você precisa também armazenar o Salt gerada, justamente para conseguir comparar o hash informado pelo usuário com o hash armazenado dentro da base de dados. Existem várias implementações dentro do .NET Framework para HMAC, cada uma utilizando um algoritmo diferente, geralmente diferenciando a quantidade de bits em seu valor hash criado. Para fins de exemplo, vamos utilizar a classe HMACSHA256 e, em relação ao código que vimos um pouco mais acima, a única alteração é apenas o nome do algoritmo: VB.NET Imports System Imports System.Security.Cryptography Using alg As New HMACSHA256 Dim msg As Byte() = Encoding.Default.GetBytes("MinhaSenha") Dim hash As Byte() = alg.ComputeHash(msg) For i As Integer = 0 To hash.Length – 1 Console.Write(hash(i).ToString("x2")) Next

Page 243: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 18

Israel Aece | http://www.projetando.net

18

End Using C# using System; using System.Security.Cryptography; using (HMACSHA256 alg = new HMACSHA256()) { byte[] msg = Encoding.Default.GetBytes("MinhaSenha"); byte[] hash = alg.ComputeHash(msg); for (int i = 0; i < hash.Length; i++) Console.Write(hash[i].ToString("x2")); } Vale lembrar que o valor do hash criado para o mesmo valor será sempre alterado, devido a natureza do algoritmo, como já vimos acima. Um detalhe importante é que as classes que fornecem algoritmos de HMAC possuem uma propriedade chamada Key, que retorna um array de bytes, representando a chave utilizada no algoritmo hash. DPAPI Com o lançamento do Windows 2000, uma nova API de proteção de dados foi criada. Essa API é chamada de DPAPI (Data Protection API) e encapsula grande parte das complexidades que estão em torno dos processos de criptografia e descriptografia. Essas classes estão integradas com o Windows, mas não são gerenciadas pelo .NET Framework. Na versão 2.0 do .NET Framework, foram introduzidas duas novas classes chamadas ProtectedData e ProtectedMemory que servem como wrapper para as classes contidas dentro da DPAPI e tornam a sua utilização bastante simples. Essas classes estão contidas dentro do namespace System.Security.Cryptography mas exige uma referência adicional para a System.Security.dll, fornecida com a versão 2.0 do .NET Framework. A primeira delas, ProtectedData é utilizada para proteger dados definidos pelo usuário. Essa classe não exige outras classes (algoritmos) de criptografia. Essa classe fornece dois métodos principais chamados de Protect e Unprotect. Ambos os métodos são estáticos e, o método Protect, além de outros parâmetros, recebe principalmente um array de bytes que representa o valor a ser protegido, retornando também um array de bytes contendo o valor criptografado; já o método Unprotect recebe um array de bytes representando o dado protegido (criptografado) e retorna um array de bytes contendo o conteúdo em seu formato original. É possível utilizar uma entropia quando invocamos o método Protect. Essa entropia trata-se de um valor aleatório, fornecido pela aplicação, que será utilizada pelo DPAPI na formação da chave de criptografia. O problema com o uso de um parâmetro adicional de

Page 244: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 19

Israel Aece | http://www.projetando.net

19

entropia é precisar ser armazenado com segurança pelo aplicativo, o que apresenta outro problema de gerenciamento de chaves, como já discutimos acima. É importante dizer também que, se um valor de entropia é passado para o método Protect, o mesmo valor deve ser também passado para o método Unprotect. Finalmente, ambos os métodos recebem em seu último parâmetro um enumerador do tipo DataProtectionScope. Esse enumerador fornece duas opções, a saber: Opção Descrição CurrentUser O data a ser protegido é associado com o usuário corrente e, somente

as threads que estão rodando com esse usuário é que poderão descriptografar os dados.

LocalMachine O data a ser protegido é associado com a máquina. Qualquer processo rodando dentro do computador, poderá descriptografar os dados.

O código abaixo exemplifica a utilização da classe ProtectedData, onde devemos criar uma entropia (pode ser opcional null em Visual C# e Nothing em Visual Basic .NET) e submetermos tanto a entropia quanto a mensagem a ser criptografada para os métodos Protect e Unprotect: VB.NET Imports System Imports System.Security.Cryptography Dim entropia() As Byte = {12, 73, 6, 92} Dim msg As String = "Valor a ser protegido." Dim msgEmBytes() As Byte = Encoding.Default.GetBytes(msg) Dim msgProtegida() As Byte = _ ProtectedData.Protect( _ msgEmBytes, _ entropia, _ DataProtectionScope.CurrentUser) Console.WriteLine( _ Encoding.Default.GetString( _ ProtectedData.Unprotect( _ msgProtegida, _ entropia, _ DataProtectionScope.CurrentUser))) C# using System; using System.Security.Cryptography; byte[] entropia = { 12, 73, 6, 92 }; string msg = "Valor a ser protegido."; byte[] msgEmBytes = Encoding.Default.GetBytes(msg);

Page 245: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 20

Israel Aece | http://www.projetando.net

20

byte[] msgProtegida = ProtectedData.Protect( msgEmBytes, null, DataProtectionScope.CurrentUser); Console.WriteLine( Encoding.Default.GetString( ProtectedData.Unprotect( msgProtegida, null, DataProtectionScope.CurrentUser))); Ainda dentro do DPAPI temos a classe ProtectedMemory. Como o próprio nome diz, essa classe protege os dados que residem na memória e, assim como a classe ProtectedData, não necessita de outras classes (algoritmos) de criptografia. As diferenças entre a classe ProtectedData e a classe ProtectedMemory são:

• A classe ProtectedMemory não utiliza entropia; • O enumerador que é passado para os métodos Protect e Unprotect da classe

ProtectedMemory é do tipo MemoryProtectionScope, que contém as seguintes opções:

Opção Descrição CrossProcess Todo código em qualquer processo pode descriptografar os dados. SameLogon Somente o código que está rodando dentro do mesmo contexto de

usuário que protegeu os dados poderá descriptografar os dados. SameProcess Somente o código que está rodando dentro do mesmo processo que

protegeu os dados poderá descriptografar os dados.

• Os métodos Protect e Unprotect não retornam nenhum valor. Ele utilizará o mesmo local onde está o array de bytes com o valor a ser protegido e aplicará a criptografia no mesmo lugar;

• Os dados que serão protegidos devem ter 16 bytes ou ser múltiplo de 16 bytes. O código abaixo exibe a utilização da classe ProtectedMemory: VB.NET Imports System Imports System.Security.Cryptography Dim msg As String = "1234567890poiuyt" Dim msgEmBytes() As Byte = Encoding.Default.GetBytes(msg) ProtectedMemory.Protect(msgEmBytes,

Page 246: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 21

Israel Aece | http://www.projetando.net

21

MemoryProtectionScope.SameLogon) ProtectedMemory.Unprotect(msgEmBytes, MemoryProtectionScope.SameLogon) C# using System; using System.Security.Cryptography; string msg = "1234567890poiuyt"; byte[] msgEmBytes = Encoding.Default.GetBytes(msg); ProtectedMemory.Protect(msgEmBytes, MemoryProtectionScope.SameLogon); ProtectedMemory.Unprotect(msgEmBytes, MemoryProtectionScope.SameLogon); Valores Randômicos Quando utilizamos os processos de criptografia e hashing, em alguns momentos é necessário criarmos chaves ou valores randômicos que são necessários para garantir que o algoritmo funcione como desejado. Muitas vezes esses valores são gerados automaticamente pelos algoritmos de criptografia/hashing, mas o .NET Framework disponibiliza publicamente duas classes que podemos utilizar para a geração destes valores randômicos. As classes fornecidas são: RandomNumberGenerator e RNGCryptoServiceProvider. A primeira delas, é uma classe abstrata que é a base necessária que todas as classes geradoras de valores randômicos devem herdar. Já a segunda classe, trata-se de uma classe concreta (CSP), chamada RNGCryptoServiceProvider. Ela extende a classe abstrata RandomNumberGenerator e implementa um Random Number Generator (RNG). A imagem abaixo exibe a hierarquia entre essas classes:

Imagem 8.3 – Hierarquia das classes que geram valores randômicos

A classe RNGCryptoServiceProvider é extensamente utilizada pelo próprio .NET Framework na geração de chaves e dos vetores que discutimos acima, durante a geração

Page 247: Livro de .NET - Israel Aece

Capítulo 8 - Criptografia 22

Israel Aece | http://www.projetando.net

22

automática desses valores. Essa classe fornece dois métodos chamados GetBytes e GetNonZeroBytes. O método GetBytes recebe um array de bytes onde ele irá populá-lo com uma seqüencia randômica de valores. Assim, como o método GetBytes, o método GetNonZeroBytes, também recebe um array de bytes onde o método irá populá-lo com uma seqüencia randômica, só que sem zeros. O código abaixo exibe uma possível forma de como utilizar essa classe: VB.NET Imports System Imports System.Security.Cryptography Dim csp As New RNGCryptoServiceProvider Dim salt(64) As Byte csp.GetBytes(salt) C# using System; using System.Security.Cryptography; RNGCryptoServiceProvider csp = new RNGCryptoServiceProvider(); byte[] salt = new byte[64]; csp.GetBytes(salt);

Page 248: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 1

Israel Aece | http://www.projetando.net

1

Capítulo 9 Utilizando Code Access Security – CAS Introdução Toda aplicação que utiliza o Common Language Runtime (CLR) obrigatoriamente deve interagir com o sistema de segurança do mesmo. Quando a aplicação é executada, automaticamente é avaliado se ela tem ou não determinados privilégios. Dependendo das permissões que a aplicação tem, ela poderá rodar perfeitamente ou gerar erros relacionados a segurança. Como estamos diante de um ambiente cada vez mais conectado, é muito comum expor o código de diferentes formas e diferentes locais. Muitos mecanismos de segurança concedem direitos de acesso aos recursos (arquivos e pastas por exemplo) baseando-se nas credenciais (nome de usuário e password) do usuário. Só que isso acaba sendo uma técnica perigosa, já que os usuários podem obter o código de diversos locais, inclusive locais desconhecidos, que podem expor códigos maliciosos, códigos que contém vulnerabilidades, etc.. Code Access Security (também conhecido como CAS), é um mecanismo que ajuda limitar e proteger o acesso que o código que está querendo realizar, protegendo os recursos e operações. Este capítulo abordará, em sua primeira parte, como utilizar o CAS, que é fornecido juntamente com o .NET Framework e, como configurar devidamente a aplicação para evitar problemas relacionados a segurança. Já a segunda parte, analisaremos a segunraça baseada em roles. Conceitos básicos – Code Access Security Toda aplicação que é executado sob a plataforma .NET interagi com o sistema de segurança – Code Access Security. De acordo com as permissões que ele recebe, ele pode executar de forma legal ou não, o que gerará uma exceção de segurança. As configurações de segurança local em um computador particular é o que vai decidir, em última instância, que permissões o código irá receber. Como esses configurações podem varias de computador para computador você deve assegurar que seu código terá as permissões suficientes para ser executado. Sendo assim, todo o desenvolvedor deve estar familiarizado com os seguintes conceitos de segurança, que são necessários para todas as aplicações escritas em utilizando a plataforma .NET:

• Escrever código type-safe: para habilitar o benefício da utilização do Code Access Security você deve utilizar um compilador que gere o código verifiably type-safe.

Page 249: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 2

Israel Aece | http://www.projetando.net

2

• Sintaxe imperativa e declarativa: são as duas formas que temos para interagir com o Code Access Security. Com a primeira delas, utilizamos as classes de forma programática, ou seja, como já fazemos na maioria das vezes com outros objetos; a segunda forma, declarativa, permite decorarmos os tipos com atributos que definem a forma de segunraça a ser aplicada ao membro.

• Requerimento de permissões: é a forma que o teu código informa ao runtime as permissões que ele precisa para poder ser executada. Esses requerimentos são analisados pelo runtime durante a carga do código para a memória. Esse tipo de permissão é aplicado a nível de Assembly.

• Secure Class Libraries: uma biblioteca segura é uma biblioteca que utiliza a segurança sob demanda para assegurar que o chamador tem ou não permissão para acessar um determinado recurso.

Como já vimos, o Code Access Security são as permissões que concedemos ao código para que o mesmo pode ser executado. Mas a segurança não se resume somente a isso; existe uma porção de conceitos que precisamos analisar detalhadamente para, em seguida, aplicar efetivamente em nosso código. Cada um desses conceitos são analisados a seguir. Evidence – Evidência A evidência é o conjunto de informações sobre um determinado Assembly. Entre essas informações temos a identidade e origem do Assembly. O Code Access Security utiliza essa evidência do Assembly e a política de segurança corrente do computador onde o mesmo está sendo executado, para determinar se ele possui ou não permissões suficientes para acessar um determinado recurso. Toda aplicação .NET roda em um AppDomain, sob o controle do host que cria o AppDomain e carrega os Assemblies para dentro do mesmo. O host tem acesso a evidência do(s) Assembly(ies) que está(ão) dentro deste AppDomain. Atualmente temos dois tipos principais de evidências: a nível de Host e a nível de Assembly. Por padrão, o .NET Framework utiliza somente a evidência a nível de host, que é a informação fornecida a partir do AppDomain onde a aplicação é executada. Tipicamente, a evidência do host vai informar a origem do Assembly e se ele está devidamente assinado (strong-name). Já a evidência a nível de Assembly é fornecida a partir do próprio Assembly, e pode ser definida a partir dos desenvolvedores ou administradores. A evidência de um Assembly poderá incluir: Evidência Descrição Condição (classe) All Code AllMembershipCondition Diretório da Aplicação

O local físico onde a aplicação foi instalada.

ApplicationDirectoryMembershipCondition

Hash Um código hash que é utilizado para algoritmos

HashMembershipCondition

Page 250: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 3

Israel Aece | http://www.projetando.net

3

de hashing. Publicador Assinatura do publicador

do Assembly. PublisherMembershipCondition

Site Site de origem do Assembly.

SiteMembershipCondition

StrongName Uma chave criptográfica, contendo o StrongName do Assembly.

StrongNameMembershipCondition

URL URL de origem do Assembly.

UrlMembershipCondition

Zona Zona de origem do Assembly, como por exemplo a Internet.

ZoneMembershipCondition

Para cada um dos itens acima, existe um classe que corresponde à uma condição, utilizada em um code group, que analisaremos mais tarde ainda neste capítulo. Essas classes estão informardas na terceira coluna da tabela acima. Para manipularmos isso via código, temos uma classe chamada Evidence dentro do namespace System.Security.Policy. Essa classe nada mais é que uma coleção que armazena um conjunto de objetos que representam as evidências (Host ou Assembly). O trecho de código abaixo exibe a forma de como devemos proceder para extrairmos as informações de evidência de um Assembly: VB.NET Imports System Imports System.Collections Imports System.Reflection Imports System.Security.Policy Dim a As Assembly = _ [Assembly].GetAssembly(Type.GetType("System.String")) Dim e As Evidence = a.Evidence Dim i As IEnumerator = e.GetEnumerator() While i.MoveNext() Console.WriteLine(i.Current) End While C# using System; using System.Collections; using System.Reflection; using System.Security.Policy; Assembly a = Assembly.GetAssembly(Type.GetType("System.String"));

Page 251: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 4

Israel Aece | http://www.projetando.net

4

Evidence e = a.Evidence; IEnumerator i = e.GetEnumerator(); while(i.MoveNext()) Console.WriteLine(i.Current); Permissions – Permissões As permissões, que aqui também são conhecidas como code access permissions, são os direitos de acesso a determinado recursos do computador. O .NET Framework possui muitas classes embutidas que foram desenhadas para proteger o acesso a determinados recursos do computador onde a aplicação é executada. Isso auxilia bastante, já que não precisamos escrever código necessário para efetuarmos a checagem de segurança; essas classes já tem essa finalidade. Para o exemplo, temos algumas permissões (em forma de classes) listadas abaixo: Permissão O que protege DataProtectionPermission Controla o acesso a dados criptografados em memória. EnvioronmentPermission Controla o acesso a variáveis de ambiente. EventLogPermission Controla o acesso ao Event Log do Windows. FileIOPermission Controla o acesso ao sistema do arquivos. PrintingPermission Controle o acesso as impressoras. RegistryPermission Controla o acesso ao Registry do Windows. SqlClientPermission Controla o acesso ao banco de dados SQL Server a partir do

provider, também fornecido pela plataforma. StorePermission Controla o acesso aos repositórios de certificados X.509. UIPermission Controla o acesso a criação de elementos Windows. Essas classes estão contidas dentro do namespace System.Security.Permissions e, grande parte dessas classes, herdam direta ou indiretamente de uma classe abstrata chamada CodeAccessPermission, que define toda a estrutura para as permissões. Security Policy – Políticas de Segurança As políticas de segurança determinam o mapeamento entre a evidência do Assembly que o host fornece para o mesmo e o conjunto de permissões concedidas ao Assembly. As políticas de segurança estão divididas em quatro níveis, quais estão abaixo descritos: Nível de Segurança Descrição Enterprise Especificada pelo administrador da rede. Contém a

hierarquia de code groups que serão aplicados para todos os códigos gerenciados que serão executados dentro da rede.

Page 252: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 5

Israel Aece | http://www.projetando.net

5

Machine Contém a hierarquia de code groups que serão aplicados para todos os códigos gerenciados que serão executados dentro de um determinado computador.

User Contém a hierarquia de code groups que serão aplicados para todos os códigos gerenciados que serão executados dentro de um usuário.

Application Domain (opcional) Este nível de segurança é opcional é fornece isolamento e limites de segurança para o código gerenciado que está sendo executado.

A permissão final concedida é definida uma por Assembly e, sendo assim, cada Assembly dentro da aplicação pode ter diferentes permissões. Finalmente, se desejarmos manipular as configurações de políticas de segurança via código, podemos utilizar a classe SecurityManager, que está contida dentro do namespace System.Security, que possui vários membros estáticos que permitem interagir com o sistema de segurança. Através desta classe, utilizamos o método PolicyHierarchy que retorna um enumerador com os níveis em que as políticas de segurança se encontram: VB.NET Imports System Imports System.Collections Imports System.Security Imports System.Security.Policy Dim i As IEnumerator = SecurityManager.PolicyHierarchy() While i.MoveNext() Dim p As PolicyLevel = DirectCast(i.Current, PolicyLevel) Console.WriteLine(p.Label) End While C# using System; using System.Collections; using System.Security; using System.Security.Policy; IEnumerator i = SecurityManager.PolicyHierarchy(); while (i.MoveNext()) { PolicyLevel p = (PolicyLevel)i.Current; Console.WriteLine(p.Label); } Permission Sets – Conjunto de Permissões

Page 253: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 6

Israel Aece | http://www.projetando.net

6

Todas os níveis de segurança contém uma lista de conjunto de permissões. Cada um desses conjuntos representam uma espécie de conjunto de acesso confiável a determinados recursos do computador. Para exemplificar, abaixo temos uma tabela com os conjuntos de permissões pré-definidos pelo .NET Framework: Permission Set Descrição FullTrust Fornece acesso a todos os recursos protegidos por permissões. SkipVerification Permite que a checagem de segurança não seja realizada. Execution Fornece permissão apenas para o código ser executado, não

permitindo acesso à qualquer recurso protegido. Nothing Não fornece nenhum tipo de permissão, impedindo-o de ser

executado. LocalIntranet Permite acesso para execução do código, criação de elementos a nível

de interface sem qualquer restrição, acesso ao isolated storage sem limite de quota, utilizar serviços de DNS, ler algumas variáveis de ambiente, realizar conexões com a site de onde o Assembly se originou e ler arquivos que estão dentro da mesma pasta do Assembly.

Internet Permite acesso para execução do código, criar janelas e caixas de diálogos, realizar conexões com a site de onde o Assembly se originou e acessar o isolated storage com quota.

Everything Fornece todas as permissões padrões, exceto as permissões para a opção SkipVerification.

O trecho de código abaixo utiliza a classe SecurityManager para recuperar todos os níveis de políticas de segurança (enterprise, machine e user) e seus respectivos conjuntos de permissões da máquina em que o código é executado: VB.NET Imports System Imports System.Collections Imports System.Security Imports System.Security.Policy Dim i As IEnumerator = SecurityManager.PolicyHierarchy() While i.MoveNext() Dim p As PolicyLevel = DirectCast(i.Current, PolicyLevel) Console.WriteLine(p.Label) Dim np As IEnumerator = p.NamedPermissionSets.GetEnumerator() While np.MoveNext() Dim pset As NamedPermissionSet = _ DirectCast(np.Current, NamedPermissionSet) Console.WriteLine("\tPermission Set: \n\t\t Name: {0}\n\t\t Description: {1}", pset.Name, pset.Description) End While

Page 254: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 7

Israel Aece | http://www.projetando.net

7

End While C# using System; using System.Collections; using System.Security; using System.Security.Policy; IEnumerator i = SecurityManager.PolicyHierarchy(); while (i.MoveNext()) { PolicyLevel p = (PolicyLevel)i.Current; Console.WriteLine(p.Label); IEnumerator np = p.NamedPermissionSets.GetEnumerator(); while (np.MoveNext()) { NamedPermissionSet pset = (NamedPermissionSet)np.Current; Console.WriteLine("\tPermission Set: \n\t\t Name: {0}\n\t\t Description: {1}", pset.Name, pset.Description); } } Code Groups – Grupos de códigos Os code groups são o coração do sistema de segurança do .NET Framework. Ele consiste em uma expressão condicional que, se for atendida, concede um determinado conjunto de permissões (permission set) que estão associadas ao code group. Em tempo de execução, a condição é avaliada comparando as informações do code group com o a evidência que foi extraída do Assembly. Para exemplificar, podemos dizer que ele somente terá acesso ao sistema de arquivos, se o publicador do Assembly for a Empresa “ABC”. Os code groups são armazenados em um formato de uma “árvore” lógica e, se a condição “A” não for atendida, as condições abaixo dela não serão analisadas e, conseqüentemente, o código não terá acesso as permissões que a mesma poderia vir a conceder. A imagem abaixo ilustra de forma bem clara a hierarquia dos code groups e como eles são avaliados em tempo de execução:

Page 255: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 8

Israel Aece | http://www.projetando.net

8

Imagem 9.1 – Avaliando as condições.

Como podemos analisar, quando um determinado code group não atende a um determinado critério (quadrados na cor vermelha), as permissões relacionadas a ele são negadas e, conseqüentemente, o que vem abaixo (quadrados na cor laranja) não é avaliado. Esse processo é repetido para todos os níveis das políticas de segurança (enterprise, machine e user). Em tempo de execução, essas condições são transformadas em classes do tipo xxxMembershipCondition que vimos um pouco mais acima. Essas classes implementam a Interface IMembershipCondition que define um teste para determinar se o código do Assembly é membro de um determinado code group. Stack Walk Uma das partes essencias do sistema de segurança é o processo que chamamos de stack walk. Quando um determinado método é chamado, os dados referente ao mesmo (parâmetros que são passados para o método, o endereço de retorno quando o método retornar e variáveis locais) são colocados em uma espécie de pilha de chamadas, call stack. Cada um desses “registros” são também chamados de stack frame. Em determinados estágios da execução do código, a thread que está executando pode precisar acessar um recurso protegido, como por exemplo, o sistema de arquivos. Antes de efetivamente conceder acesso a esse determinado recurso, sob demanda, uma verificação é efetuada em toda a call stack, analisando se todos os chamadores possuem

Page 256: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 9

Israel Aece | http://www.projetando.net

9

direitos ao recurso solicitado. Neste momento, se algum dos chamadores não tiver a permissão necessária, uma exceção é atirada. Esse processo é chamado de stack walk. Para fazer uma analogia em um mundo real, imagine que há uma pessoa que deseja alugar um livro em uma biblioteca mas ela não tem um cadastro na mesma. Imagine que essa pessoa pedi um favor para alguém que tenha esse cadastro, para que ele possa pegar o livro na biblioteca e, em seguida, o emprestar. Como o bibliotecário nada sabe sobre o que se passa, ele analisará o cadastro da pessoa que for diretamente até a biblioteca e, estando com o cadastro correto, alugará o livro sem maiores problemas. Esse processo é mostrado através da imagem abaixo:

Imagem 9.2 – Luring Attack.

Apesar dessa forma ser executada sem maiores problemas, isso não traz uma segurança para a biblioteca. Trazendo para o mundo de desenvolvimento de software, entendemos isso como um ataque, chamado de Luring Attack. O luring attack é um tipo de ataque que eleva os privilégios de quem o executa, concedendo mais privilégios do que realmente ele possui. Para evitar o luring attack, o .NET é capaz de analisar toda a cadeia de chamadores e analisar se todos eles possuem ou não os direitos necessários quando algum recurso protegido é requisitado. Ainda dentro do nosso exemplo, quando o José Torres chegar ao bibliotecário para alugar o livro, o bibliotecário irá analisar todos os chamadores que fazem parte do processo e, irá identificar que o Manoel Costa não tem cadastro na biblioteca, o que banirá o aluguel do livro. A imagem abaixo ilustra esse processo:

Page 257: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 10

Israel Aece | http://www.projetando.net

10

Imagem 9.3 – Evitando o Luring Attack.

Como vimos anteriormente, temos classes que são nomeadas com um sufixo Permission. Essas classes herdam da classe abstrata CodeAccessPermission e protegem determinados recursos de um computador. Essa classe abstrata fornece quatro principais métodos (de instância) que, são utilizados para determinar se o código possui ou não acesso ao recurso que a instância representa. Esses métodos são listados e descritos através da tabela abaixo: Método Descrição Assert Concede acesso ao recurso protegido pela instância mesmo que os

chamadores não tenham permissão para isso. Esse método pode ser utilizado quando a sua biblioteca necessita acessar um recurso protegido de forma completamente oculta aos chamadores. De qualquer forma, utilize esse método com muito cuidado, porque ele pode deixar a sua aplicação/biblioteca vulnerável a ataques.

Demand Ao chamar esse método, a checagem é realizada em toda a stack (de cima para baixo), forçando uma exceção do tipo SecurityException ser atirada, se algum dos chamadores dentro da stack não tiver a permissão para o recurso protegido pela instância. Esse método é geralmente utilizado por bibliotecas que precisam assegurar que o chamador tem acesso a um recurso protegido.

Page 258: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 11

Israel Aece | http://www.projetando.net

11

Deny Previne os chamadores dentro da stack de acessar o recurso protegido pela instância, mesmo que eles tenham permissão para isso. A imagem abaixo ilustra o processo da utilização deste método:

Imagem 9.4 – Utilização do método Deny.

Como sabemos, o método Demand executa a checagem dentro da stack para verificar se todos os chamadores possuem a permissão necessária para acessar o recurso. Na imagem acima, quando o Método B é avaliado, vemos que dentro dele, o método Deny foi chamado, o que evita que a permissão seja concedida. Note que, ao encontrar a chamada para o método Deny, o Método A não chega a ser avaliado, pois independente do resultado, a permissão não será concedida.

PermitOnly Em essência, o método PermitOnly tem o mesmo efeito do método Deny, mas define uma condição diferente de como a segurança deve ser analisada para conceder ou negar acesso à um determinado recurso. Ao invés de dizer que um recurso específico não pode ser acessado (é o que o método Deny faz), o método PermitOnly informa somente os recursos que você quer conceder acesso. Se você chamar o método PermitOnly em uma permissão X, é o mesmo que chamar que chamar o método Deny para todas as permissões, com exceção da permissão X.

Além desses métodos, a classe CodeAccessPermission ainda fornece quatro métodos estáticos que são utilizados para reverter alguma das “ordens” acima, que foram aplicadas pelos métodos Assert, Deny ou PermitOnly, fazendo com que essas “ordens” sejam desfeitas. A tabela abaixo descreve cada um desses métodos:

Page 259: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 12

Israel Aece | http://www.projetando.net

12

Método Descrição RevertAll Esse método desfaz todos as “ordens” realizadas pelos métodos

Assert, Deny ou PermitOnly e, se nenhum desses métodos foi previamente invocado, uma exceção do tipo ExecutionEngineException será atirada.

RevertAssert Remove qualquer instrução gerada pelo método Assert dentro de um mesmo frame. Se o método Assert não foi previamente invocado, uma exceção ExecutionEngineException do tipo será atirada.

RevertDeny Remove qualquer instrução gerada pelo método Deny dentro de um mesmo frame. Se o método Deny não foi previamente invocado, uma exceção ExecutionEngineException do tipo será atirada.

RevertPermitOnly Remove qualquer instrução gerada pelo método PermitOnly dentro de um mesmo frame. Se o método PermitOnly não foi previamente invocado, uma exceção ExecutionEngineException do tipo será atirada.

Você deve utilizar esses métodos com extremo cuidado porque eles modificam a forma com que o Code Access Security avalia a stack walk, o que pode possibilitar que sua aplicação sofra com ataques do tipo luring attack, como vimos nas imagens acima. Geralmente, a checagem de segurança examina todos os chamadores que estão dentro da stack para assegurar que cada um deles possuem as devidas permissões para acessar o recurso protegido que está sendo solicitado. Entretanto, através dos métodos acima podemos sobrescrever esse comportamento e, conseqüentemente, customizar a forma com que o Code Access Security concede ou nega o acesso à um determinado recurso. Esse processo é conhecido como “Overriding Security Checks”. Toda vez que um método chama outro, um novo frame é gerado dentro da stack para armazenar informações a respeito do método que está sendo invocado. Cada um desses frames contém informações sobre a chamada de qualquer um dos métodos Assert, Deny ou PermitOnly e, se o chamador utiliza mais que um desses métodos dentro do mesmo local (método), o runtime aplica as seguintes regras:

• Se, durante a checagem da stack walk, o runtime descobrir mais que uma chamada para um mesmo método (Assert, Deny ou PermitOnly) dentro do mesmo frame, o segundo causará uma exceção.

• Quando houver chamadas diferentes aos métodos Assert, Deny ou PermitOnly dentro do mesmo frame, o runtime os processará na seguinte ordem: PermitOnly, Deny e, finalmente, o Assert.

E como vimos na tabela dos métodos Revert***, você utiliza-os quando desejar reverter qualquer uma das operações previamente executadas. Ferramentas

Page 260: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 13

Israel Aece | http://www.projetando.net

13

Com exceção das classes que o .NET Framework fornece para customizarmos via código o que a nossa aplicação necessita para poder funcionar, ainda há duas ferramentas úteis, quais foram disponibilizadas com a instalaçã do .NET Framework, que permitem interagirmos com o sistema de segurança, modificando as políticas de segurança da máquina, do usuário ou da rede. A primeira delas, é chamada de .NET Framework 2.0 Configuration. Essa ferramenta permite-nos configurarmos não somente a parte de segurança, mas o GAC e outras coisas que estão fora do escopo deste capítulo. Se reparar na imagem abaixo, temos uma opção chamada Runtime Security Policy, onde podemos customizar os code groups e permissions sets para os níveis de segurança existentes. A imagem abaixo ilustra a interface desta ferramenta:

Imagem 9.5 - .NET Framework 2.0 Configuration.

Para acessar essa ferramenta, vá até as Ferramentas Administrativas que está dentro do Painel de Controle do Windows e clique em Microsoft .NET Framework 2.0 Configuration. Além desta ferramenta gráfica, ainda existe uma outra chamada CasPol.exe. Trata-se de um utilitário de linha de comando que permite você interagir com o sistema de segurança do .NET Framework e, basicamente, fornece as mesmas funcionalidades que a interface acima.

Page 261: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 14

Israel Aece | http://www.projetando.net

14

Esse utilitário é disponibilizado junto com o .NET Framework e encontra-se localizado no seguinte endereço: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727. Como esses utilitários são executados a partir de linha de comando e você optar por abrir o Visual Studio 2005 Command Prompt, não será necessário digitar o caminho todo até o executável para executá-lo. A sintaxe para a sua utilização é simples: C:\caspol <opções> <argumentos> Entre as opções que são aceitas, temos algumas delas listadas através da tabela abaixo: Opção Descrição -l Lista todas as informações disponíveis para a policy level padrão, que é o

nível Machine. -lg Lista todos os code groups para a policy level padrão. -lp Lista todas as permissions sets. -ag Adiciona um novo code group na policy level padrão. -url Define a URL para um endereço HTTP ou FTP, indicando onde o

Assembly se originou. Isso será adicionado em forma de uma condição (UrlMembershipCondition).

-n O nome para o novo code group. -exclusive Definindo esta opção como “on” indica que qualquer Assembly que atender

a condição definida pelo code group que você está criando, será associado com a permission set para esta policy level. Isso pode ser utilizado para nível de testes, pois irá garantir que o código que está rodando não recebe a permissão de FullTrust.

-cg Altera um code group existente. -rg Remove um code group existente. Abaixo é exibido algumas possíveis formas de combinar essas opções e argumentos para a utilização deste utilitário: C:\caspol –l C:\caspol –lg C:\caspol –ag 1 –url file:///C:/Teste/* Internet –n Grupo_De_Teste –exclusive on C:\caspol –rg Grupo_De_Teste

Page 262: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 15

Israel Aece | http://www.projetando.net

15

Com exceção da lista acima, temos outras opções e argumentos que podemos informar par ao utilitário caspol.exe. Para ter acessa a todas elas, vá até o MSDN Library ou, ainda no prompt de comando, digite: C:\caspol -? Avaliando as permissões Quando a aplicação é inicializada, a mesma é carregada para dentro de um processo, que é chamado também de host. Esse host extrai e examina a evidência do Assembly, podendo diferentes informações serem coletadas de acordo com a origem do mesmo. A evidência consiste, entre outras inforamções, o strong-name, zona e publicador do Assembly. Depois de capturada, essa evidência é passada para o sistema de segurança do .NET Framework para que o mesmo avalie as políticas de seguranças. A partir deste momento, baseando-se na evidência extraída do Assembly, o runtime começará a avaliar a “árvore” de code groups. Se a condição for atendida, dizemos que o Assembly é membro do code group e, além disso, o conjunto de permissões (permission set) vinculadas a essa condição será concedido ao Assembly. Caso contrário, possíveis code groups que estiverem abaixo da condição não atendida, não serão avaliados e, obviamente, não serão concedidos as possíveis permissões que ele poderia vir definir. Depois deste processo, a união dos conjuntos de permissões é computada e esse processo é repetido para cada nível das políticas de segurança (policy levels). Depois de todos os níveis avaliados (Enterprise, Machine, User e AppDomain), o Assembly receberá a interseção de todos os grupos de permissões entre os níveis. A imagem abaixo ilustra esse todo esse processo que, a primeira vista, parece ser complicado:

Page 263: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 16

Israel Aece | http://www.projetando.net

16

Imagem 9.6 – Avaliando as permissões de um Assembly

Como podemos notar, os níveis Enterprise, Machine, User e AppDomain são avaliados e somente são concedidos as permissões que estão contidas em todos os níveis que, no exemplo acima, são P2 e P5. Isto evitará que um usuário individual ou a aplicação conceda permissões adicionais que não foram concedidas pelo administrador (Enterprise). Ainda é possível adicionar atributos a nível de Assembly que permite especificarmos as solicitações de permissões necessárias, mínimas e opcionais que o Assembly necessita para poder trabalhar. Abaixo veremos cada uma dessas opções:

• Permissões Mínimas: especificada a partir do atributo RequestPermission, permite especificar as permissões mínimas que são necessárias para que o Assembly para executar o seu trabalho. Se essas permissões não forem concedidas, ao carregar o Assembly o código não será executado e uma exceção do tipo PolicyException será atirada.

• Permissões Opcionais: especifica as permissões que seu código pode utilizar para executar mas, se não for concedida, o mesmo será capaz de executar. Neste caso, é extremamente importante que você utilize o tratamento de erros ou, de alguma outra forma, monitorar o trecho do código que exige essa permissão, pois, se não fizer isso, uma exceção pode ocorrer e danificar a sua aplicação.

• Permissões Recusadas: especifica as permissões que seu código jamais deverá receber, mesmo se as políticas de segurança as concedam.

O trecho de código abaixo exibe como configurar esses parâmetros dentro do arquivo AssemblyInfo.vb ou AssemblyInfo.cs: VB.NET - (AssemblyInfo.vb) Imports System.Security.Permissions

Page 264: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 17

Israel Aece | http://www.projetando.net

17

<Assembly: EnvironmentPermission(SecurityAction.RequestMinimum, Read := "windir")> <Assembly: RegistryPermission(SecurityAction.RequestOptional)> <Assembly: FileIOPermission(SecurityAction.RequestRefuse, Read := "C:\")> C# - (AssemblyInfo.cs) using System.Security.Permissions; [assembly: EnvironmentPermission(SecurityAction.RequestMinimum, Read="windir")] [assembly: RegistryPermission(SecurityAction.RequestOptional)] [assembly: FileIOPermission(SecurityAction.RequestRefuse, Read = "C:\\")] Como podemos notar no código acima, definimos que o Assembly precisa, no mínimo, de permissão suficiente para ler a variável de ambiente chamada windir; além disso, opcionalmente, pode ter acesso ao registry do Windows onde a aplicação estiver sendo executada e, finalmente, negamos o acesso a leitura da unidade C da máquina onde a aplicação estiver sendo executada. Ao especificar a permissão, definimos se ela vai ser mínima, opcional ou recusada através de um enumerador chamado SecurityAction. Para encerrar, depois de todas essas checagens, o Assembly recebe o que chamamos de Final Permission Grant. Essa permissão final é definida através do resultado da seguinte operação: FG = SP ∩ ((M U O) – R) Onde FG é a permissão final (final grant), SP é o grupo de permissões (permission set) que o Assembly recebe das políticas de segurança; depois disso, as permissões mínimas (M) unem-se as permissões opcionais (O), excluíndo as permissões recusadas (R). Com o resultado disso, é feito uma interseção com o grupo de permissões concedidos pelas políticas de segurança e, finalmente, atribuído a permissão final do Assembly. Implementando a segurança no código No código, o que precisamos fazer é instanciar a classe concreta da permissão, como por exemplo, a classe FileIOPermission, especificar os parâmetros necessários que a mesma exige e, através dos métodos que vimos acima (Demand, Assert, Deny ou PermitOnly) modificamos o stack walk e, conseqüentemente, customizamos o acesso ao recurso protegido.

Page 265: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 18

Israel Aece | http://www.projetando.net

18

Permissões Declarativas vs. Imperativas Como vimos acima, há duas formas de aplicarmos a segurança em uma aplicação .NET. Essas formas são conhecidas como declarativa e imperativa. Abaixo há um exemplo de cada um destes estilos: Forma Declarativa: VB.NET <FileIOPermission(SecurityAction.Assert, Read:="C:\")> _ Public Sub ReadFile() ‘... End Sub C# [FileIOPermission(SecurityAction.Assert, Read:="C:\\")] public void ReadFile() { //... } Forma Imperativa: VB.NET Public Sub ReadFile() Dim f As New FileIOPermission(FileIOPermissionAccess.Read, "C:\") f.Assert() ‘... End Sub C# public void ReadFile() { new FileIOPermission(FileIOPermissionAccess.Read, "C:\\").Assert(); //... } Há algumas razões para escolher entre um estilo e outro. Um das grandes diferenças é que o estilo declarativo exige que todas as regras de segurança sejam definidas em tempo de compilação, permitindo apenas aplicar essas definições em métodos, classes ou Assemblies.

Page 266: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 19

Israel Aece | http://www.projetando.net

19

Já o estilo imperativo te dá uma maior flexibilidade ao estilo declarativo, pois permite que você manipule a segurança em tempo de execução, podendo assim, determinar o momento preciso de aplicar a checagem de segurança. Infelizmente, esse modo não permite extrairmos informações de metadados relacionadas a segurança. Essa ferramenta, chamada Permission View (Permview.exe), permite extrairmos essas informações de metadados somente a partir do estilo declarativo. Esse utilitário é fornecido somente com a versão 1.x do .NET Framework. Para utilizá-lo, é necessário apenas informar caminho até o Assembly que deseja visualizar as informações. O código abaixo ilustra como devemos proceder para invocar esse utilitário: C:\ permview Aplicacao.exe Segurança baseada em papéis (roles) A segurança baseada em roles permite aos desenvolvedores controlarem o acesso das aplicações construídos sob a plataforma .NET baseando-se na identifidade do usuário. Neste caso, trabalhamos com dois conceitos chamados: identity e principal. O primeiro deles, identity, encapsula informações a respeito da identificação do usuário que está sendo validado. Entre essas informações temos o nome do usuário e o tipo de autenticação. Basicamente, as identities são responsáveis pela autenticação do usuário. O .NET Framework fornece três tipos de objetos de identidade:

• Windows Identity: representa a identidade od usuário e o método de autenticação que é suportado pelo sistema operacional Windows. Além disso, esse tipo de identidade, fornece a possibilidade de personificarmos o usuário corrente para um outro usuário, talvez com mais ou menos privilégios para poder acessar um recurso protegido que, o usuário corrente talvez não tenha acesso. A classe que representa essa identidade é a WindowsIdentity.

• Generic Identity: representa a identidade do usuário baseando em uma autenticação customizada que é definida pela aplicação. A classe GenericIdentity implementa esse tipo de identidade.

• Custom Identity: representa uma identidade que encapsula as informações customizadas do usuário. Qualquer identidade customizada deve implementar a Interface IIdentity que, por sua vez, fornece três membros que são listados na tabela abaixo:

Membro Descrição Name Retorna uma string contendo o nome corrente do usuário. IsAuthenticated Retorna um valor booleano indicando se o usuário está ou não

autenticado.

Page 267: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 20

Israel Aece | http://www.projetando.net

20

AuthenticationType Retorna o uma string contendo o tipo de autenticação do usuário corrente.

Já o principal representa o contexto de segurança em que o código está sendo executado. Basicamente, as principals são responsáveis pela autorização do usuário. O .NET Framework fornece três tipos de objetos relacionadas ao principal:

• Windows Principal: representa usuários do sistema operacional Windows e suas respectivas roles. Cada role representa um grupo que podem conter membros. A classe que representa essa principal é a WindowsPrincipal.

• Generic Principal: representa usuários e suas respectivas roles, mas de forma independente ao sistema operacional. A classe GenericPrincipal implementa este tipo de principal.

• Custom Principal: representa uma principal que encapsula a autorização de um determinado usuário. Qualquer principal customizada deve implementar a Interface IPrincipal.

A Interface IPrincipal possui apenas dois membros e, sendo assim, todas as classes relacionadas a principal também os possuem, já que implementam direta ou indiretamente a Interface IPrincipal. Os membros desta Interface são listados abaixo: Membro Descrição Identity Retorna um objeto que implementa a Interface IIdentity representando a

identidade do usuário corrente. IsInRole Dado uma string contendo o nome da role (grupo), retorna um valor

booleano indicando se o usuário corrente está ou não contido neste grupo. Todas as Interfaces e classes que vimos acima estão contidas dentro do namespace System.Security.Principal. Abaixo é exibido uma imagem que ilustra as classes e Interfaces que vimos acima.

Page 268: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 21

Israel Aece | http://www.projetando.net

21

Imagem 9.7 – Estrutura das classes identity e principal.

Dentro do namespace System.Threading existe uma classe chamada Thread. Essa classe determina como controlar uma thread dentro da aplicação. Essa classe, entre vários membros, possui uma propriedade estática chamada CurrentPrincipal que recebe e retorna uma instância de um objeto que implementa a Interface IPrincipal. É através desta propriedade que devemos definir qual será a identity e principal que irá representar o contexto do segurança para a thread atual. Sendo assim, temos que, de acordo com o tipo de autenticação/autorização que iremos adotar na aplicação, devemos criar a instância de uma identiy e principal e, em seguida, definí-la na propriedade CurrentPrincipal da classe Thread. Abaixo temos um exemplo utilizando as classes identity e principal relacionados ao sistema operacional Windows e também a forma genérica: Windows VB.NET Imports System.Threading Imports System.Security.Principal Dim identity As WindowsIdentity = WindowsIdentity.GetCurrent() Thread.CurrentPrincipal = New WindowsPrincipal(identity) Console.WriteLine(Thread.CurrentPrincipal.Identity.Name)

Page 269: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 22

Israel Aece | http://www.projetando.net

22

Console.WriteLine(Thread.CurrentPrincipal.IsInRole("Admin").ToString()) C# using System.Threading; using System.Security.Principal; WindowsIdentity identity = WindowsIdentity.GetCurrent(); Thread.CurrentPrincipal = new WindowsPrincipal(identity); Console.WriteLine(Thread.CurrentPrincipal.Identity.Name); Console.WriteLine(Thread.CurrentPrincipal.IsInRole("Admin").ToString()); Genérica VB.NET Imports System.Threading Imports System.Security.Principal Dim identity As New GenericIdentity("Jose") Dim roles() As String = { "Admin", "RH", "Financeiro" } Thread.CurrentPrincipal = New GenericPrincipal(identity, roles) Console.WriteLine(Thread.CurrentPrincipal.Identity.Name) Console.WriteLine(Thread.CurrentPrincipal.IsInRole("Admin").ToString()) C# using System.Threading; using System.Security.Principal; GenericIdentity identity = new GenericIdentity("Jose"); string[] roles = new string[] { "Admin", "RH", "Financeiro" }; Thread.CurrentPrincipal = new GenericPrincipal(identity, roles); Console.WriteLine(Thread.CurrentPrincipal.Identity.Name); Console.WriteLine(Thread.CurrentPrincipal.IsInRole("Admin").ToString()); Utilizando a forma genérica, você pode customizar o repositório de onde você pode recuperar os dados de acesso e também os grupos que o usuário está contido e, utilizá-los na aplicação. Um exemplo é buscar os dados em um banco de dados e, se encontrado, criar os objetos identity e principal com os dados do usuário.

Page 270: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 23

Israel Aece | http://www.projetando.net

23

Personificação Personificar é a habilidade que a thread possui para executar uma determinada tarefa em um contexto de segurança que é diferente do contexto que foi criado para o processo da própria thread. Uma das principais razões para utilizar a personificação é quando você precisa acessar um determinado recurso que, o usuário corrente não possui privilégios e, neste caso, personificamos o mesmo para que a tarefa seja executada através de um outro usuário, com maiores privilégios e, conseqüentemente, que tenha acesso ao recurso necessitado. A classe WindowsIdentity fornece um método que permite personificarmos o usuário. Esse método chama-se Impersonate que, através da instância da classe WindowsIdentity, irá personificar para o usuário que está definido nela. Se bem sucedido, esse método retorna um objeto do tipo WindowsImpersonationContext que representa o usuário do Windows já no contexto personificado. Essa classe também fornece um método chamado Undo que devemos utilizar para reverter a personificação, voltando ao contexto do usuário original. O exemplo abaixo mostra uma função que, via PInvoke, verifica se existe ou não o usuário “Teste” e, se existir, cria um objeto do tipo WindowsIdentity, passando como parâmetro o token do usuário validado para efetuar a personificação para o mesmo. Repare também que o método Undo é chamado dentro do bloco finally para garantir que o mesmo será executado caso algum problema ocorra e assim, evitar com que o usuário fique personificado. VB.NET Imports System Imports System.Security.Principal Imports System.Runtime.InteropServices Public Class Program <DllImport("advapi32.dll")> _ Public Shared Function LogonUser( _ ByVal lpszUsername As String, _ ByVal lpszDomain As String, _ ByVal lpszPassword As String, _ ByVal dwLogonType As Integer, _ ByVal dwLogonProvider As Integer, _ ByRef phToken As IntPtr) As Boolean End Function Public Shared Sub Main() Impersonate() End Sub

Page 271: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 24

Israel Aece | http://www.projetando.net

24

Public Shared Sub Impersonate() Dim token As IntPtr Try If LogonUser("Teste", String.Empty, "123456", 2, 0, token) Then Dim identity As New WindowsIdentity(token) Dim impersonationContext As WindowsImpersonationContext = Nothing Try Console.WriteLine("1: " + WindowsIdentity.GetCurrent().Name) impersonationContext = identity.Impersonate() Console.WriteLine("2: " + WindowsIdentity.GetCurrent().Name) Finally impersonationContext.Undo() Console.WriteLine("3: " + WindowsIdentity.GetCurrent().Name) End Try End If Finally token = IntPtr.Zero End Try End Sub End Class C# using System; using System.Security.Principal; using System.Runtime.InteropServices; class Program { [DllImport("advapi32.dll")] private static extern bool LogonUser( String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken); static void Main() { WindowsIdentity identity = WindowsIdentity.GetCurrent(); Impersonate(); } private static void Impersonate()

Page 272: Livro de .NET - Israel Aece

Capítulo 9 – Utilizando Code Access Security – CAS 25

Israel Aece | http://www.projetando.net

25

{ IntPtr token; try { if (LogonUser("Teste", string.Empty, "123456", 2, 0, out token)) { WindowsIdentity identity = new WindowsIdentity(token); WindowsImpersonationContext impersonationContext = null; try { Console.WriteLine("1: " + WindowsIdentity.GetCurrent().Name); impersonationContext = identity.Impersonate(); Console.WriteLine("2: " + WindowsIdentity.GetCurrent().Name); } finally { impersonationContext.Undo(); Console.WriteLine("3: " + WindowsIdentity.GetCurrent().Name); } } } finally { token = IntPtr.Zero; } } }

Page 273: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 1

Israel Aece | http://www.projetando.net

1

Capítulo 10 Envio de Mensagens (E-mails) Introdução É muito comum todo e qualquer tipo de aplicação enviar e-mails para satisfazer um determinado processo ou notificar alguém de que uma condição foi alcançada. Felizmente, o .NET Framework fornece intrinsicamente, sem a necessidade de utilizar componentes de terceiros, um conjunto de classes que podem ser utilizadas para construir e enviar e-mails. Nas versões 1.x do .NET Framework, as classes relacionado ao envio de e-mails estavam contidas em uma namespace chamado System.Web.Mail, dentro do Assembly System.Web.dll. Como envio de e-mails não é uma exclusividade de aplicações Web, isso ficou um pouco confuso e ainda, é necessário fazermos a referência ao Assembly System.Web.dll em uma aplicação Windows Forms se lá quisermos enviar e-mails. Definitivamente isso não faz sentido. Na versão 2.0 do .NET Framework isso foi mudado e agora essas classes estão contidas dentro do namespace System.Net.Mail, prontas para serem utilizadas. Essas classes fornecem toda a infraestrutura para a criação de e-mails, possibilidade de anexar vários destinatários (inclusive em cópia carbono), anexar arquivos e embutir arquivos (imagens) no corpo da mensagem, utilizadas para compor a mensagem. Além da criação, ainda temos uma classe importante, chamada de SmtpClient, que encapsula todo o processo de envio da mensagem. Através deste capítulo, analisaremos as principais classes e como proceder para criar e enviar e-mail a partir de aplicações .NET. Criando um Email Para que possamos construir um e-mail precisamos utilizar a classe MailMessage. Como o próprio nome diz, essa classe representa um e-mail, contendo os arquivos em anexo, o remetente, destinatário, assunto, corpo, etc.. A instância desta classe pode ser passada para o método Send da classe SmtpClient para que possa definitivamente enviar o e-mail ao destinatário. Analisaremos essa classe com mais detalhes nas próximas seções. Para familiarizarmos melhor com a classe MailMessage, a tabela abaixo mostra as principais propriedades que ela expõe e que podemos utilizar para configurá-la: Propriedade Descrição AlternateViews Esta coleção representa cópias de um mesmo e-mail em

diferentes formatos, ou seja, você pode ter uma versão do e-mail em HTML e uma versão do e-mail em texto puro, para aqueles clientes que não conseguem visualizar o conteúdo da mensagem em formato HTML.

Attachments Uma coleção de elementos do tipo Attachment que

Page 274: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 2

Israel Aece | http://www.projetando.net

2

indica um determinado arquivo que será anexado à mensagem.

Bcc Uma coleção de elementos do tipo MailAddress que indica os endereços que receberão uma cópia do e-mail, mas ficarão ocultos. Os destinatários colocados nesta seção não são visualizados pelos receptores do e-mail.

Body Uma string contendo o corpo da mensagem. Essa string poderá conter tags HTML se o corpo do e-mail for criado baseando-se em HTML.

BodyEncoding Define o encoding do corpo do e-mail. CC Uma coleção de elementos do tipo MailAddress que

indica os endereços que estão copiados no e-mail e que, conseqüentemente, receberão uma cópia do e-mail.

DeliveryNotificationsOptions Especifica se uma notificação deverá ser enviado ao remetente do e-mail. Essa propriedade é definida com uma das opções especificados pelo enumerador DeliveryNotificationOptions, que fornece os seguintes valores:

• Delay – Notifica se a entrega está atrasada. • Never – Nunca notifica. • None – Sem notificação. • OnFailure – Notifica se a entrega falhou. • OnSuccess – Notifica se a entrega foi feita com

sucesso. From Recebe um objeto do tipo MailAddress contendo as

informações a respeito do remetente da mensagem. Headers Trata-se de uma coleção do tipo NameValueCollection,

com as chaves do cabeçalho que são transmitidos com o e-mail.

IsBodyHtml Especifica um valor booleano indicando se o corpo da mensagem está ou não em formato HTML.

Priority Indica a prioridade da mensagem através do enumerador MailPriority. As opções que ele fornece são:

• High – Prioridade alta. • Low – Prioridade baixa. • Normal – Prioriedade normal.

ReplyTo Recebe um objeto do tipo MailAddress que é utilizado, ao invés da propriedade From, quando o usuário responder ao e-mail.

Sender Recebe um objeto do tipo MailAddress que é utilizado

Page 275: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 3

Israel Aece | http://www.projetando.net

3

como remetente do e-mail. Subject Uma string contendo o assunto do e-mail. SubjectEncoding Define o encoding do corpo do assunto. To Recebe um objeto do tipo MailAddress contendo as

informações a respeito do destinatário da mensagem. A classe MailAddress que mencionamos várias vezes na tabela acima, trata-se de um objeto que representa um endereço de correio eletrônico, independente se ele é remetente ou destinatário. Essa classe tem apenas quatro propriedades: Address, DisplayName, Host e User. A primeira delas, Address, recebe uma string contendo o endereço de e-mail; a seguir, temos a propriedade DisplayName, que também recebe uma string onde podemos definir o nome amigável a ser exibido que alguns leitores de e-mail utilizam para exibir; a terceira delas, a propriedade Host, trata-se de uma propriedade de somente leitura que retorna uma string contendo o host informado na propriedade Address; finalmente, a propriedade User, também retorna uma string contendo o nome do usuário (a primeira parte, antes do @) informado na propriedade Address. As propriedade To, CC e Bcc expõe uma coleção fortemente tipada do tipo MailAddressCollection que somente operam com objetos do tipo MailAddress. O trecho de código abaixo exemplifica a utilização da classe MailMessage em conjunto com a classe MailAddress: VB.NET Imports System.Net.Mail Dim de As New MailAddress("[email protected]", "Israel Aéce - Via .NET") Dim para As New MailAddress("[email protected]", "Israel Aéce") Dim msg As New MailMessage(de, para) msg.Attachments.Add(New Attachment("Teste.txt")) msg.Subject = "Teste de envio no .NET" msg.Body = "<b>E-mail enviado via .NET 2.0</b>" msg.IsBodyHtml = True C# using System.Net.Mail; MailAddress de = new MailAddress("[email protected]", "Israel Aéce - Via .NET"); MailAddress para = new MailAddress("[email protected]", "Israel Aéce"); MailMessage msg = new MailMessage(de, para); msg.Attachments.Add(new Attachment("Teste.txt")); msg.Subject = "Teste de envio no .NET"; msg.Body = "<b>E-mail enviado via .NET 2.0</b>"; msg.IsBodyHtml = true;

Page 276: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 4

Israel Aece | http://www.projetando.net

4

Uma alternativa ao código acima para deixar o conteúdo do e-mail mais flexível a diversos leitores, é utilizar os AlternateViews para criar versões diferentes do mesmo corpo do e-mail, para que seja possível que usuários que suportam HTML e aqueles não suportam, consigam visualizar a mensagem. Neste caso, o código tem uma mudança um pouco radical na definição do corpo do e-mail, ou seja, não será mais necessário definir a propriedade Body, pois criaremos isso a partir da classe AlternateView. O código abaixo ilustra apenas a criação dos AlternateViews, mantendo o restando do código idêntico ao que temos acima: VB.NET Imports System.Net.Mail Imports System.Net.Mime msg.AlternateViews.Add( _ AlternateView.CreateAlternateViewFromString( _ "<b>E-mail enviado via .NET 2.0 - HTML</b>", _ Nothing, _ MediaTypeNames.Text.Html)) msg.AlternateViews.Add( _ AlternateView.CreateAlternateViewFromString( _ "E-mail enviado via .NET 2.0 - Plain Text", _ Nothing, _ MediaTypeNames.Text.Plain)) C# using System.Net.Mail; using System.Net.Mime; msg.AlternateViews.Add( _ AlternateView.CreateAlternateViewFromString( "<b>E-mail enviado via .NET 2.0 - HTML</b>", null, _ MediaTypeNames.Text.Html)); msg.AlternateViews.Add( AlternateView.CreateAlternateViewFromString( "E-mail enviado via .NET 2.0 - Plain Text", null, MediaTypeNames.Text.Plain)); O método estático CreateAlternateViewFromString retorna um objeto do tipo AlternateView com o body pré-configurado. Para esse mesmo método, passamos como

Page 277: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 5

Israel Aece | http://www.projetando.net

5

último parâmetro, o tipo da visualização, indicando através da classe MediaTypeNames. A imagem abaixo ilustra o e-mail recebido dentro do Microsoft Outlook:

Imagem 11.1 – E-mail recebido no Microsoft Outlook.

Embutindo imagens como recursos A versão 2.0 do .NET Framework já traz intrinsicamente um recurso que nas versões anteriores somente conseguíamos com a utilização de componentes de terceiros; trata-se da opção de agora podermos embutir dentro do e-mail imagens que farão parte do conteúdo do mesmo. Nas versões anteriores, se não quiséssemos utilizar componentes de terceiros, tínhamos que disponibilizar em algum lugar público, geralmente imagens, que iriam fazer parte do conteúdo do email e, através do acesso via HTTP, a exibíamos no inteiro do corpo do e-mail. O ponto negativo disso é que o usuário que está lendo o e-mail depende de uma conexão ativa com a internet para que o consiga visualizar essas imagens. Com a versão 2.0 do .NET Framework, temos duas principais classes para trabalharmos com isso. São elas: AlternateView e LinkedResource. A primeira especifica diferentes cópias do conteúdo do email, ou seja, você define o e-mail com o formato e tags HTML e, se o leitor de e-mails do destinatário não suportar HTML, você pode fornecer através desta classe, uma versão em plain-text do mesmo conteúdo. Já a segunda classe, representa um recurso externo que será embutido dentro do conteúdo do email que, na maioria dos casos, é uma imagem. Depois desta classe criada, o adicionamos na coleção de LinkedResources do objeto AlternateView. O código abaixo mostra-nos como devemos proceder para conseguirmos enviar um e-mail com uma imagem embutida no corpo do mesmo:

Page 278: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 6

Israel Aece | http://www.projetando.net

6

VB.NET Imports System.Net.Mail Imports System.Net.Mime Dim de As New MailAddress("[email protected]", "Israel Aéce ") Dim para As New MailAddress("[email protected]", "Israel Aéce") Dim msg As New MailMessage(de, para) msg.Subject = "Teste de E-mail" Dim body As String = _ "<img src=""cid:Imagem1"" /><br><br><b>E-mail enviado via .NET 2.0</b>" Dim view As AlternateView = _ AlternateView.CreateAlternateViewFromString(body, Nothing, MediaTypeNames.Text.Html) Dim resource As New LinkedResource("Logo.gif") resource.ContentId = "Imagem1" view.LinkedResources.Add(resource) msg.AlternateViews.Add(view) C# using System.Net.Mail; using System.Net.Mime; MailAddress de = new MailAddress("[email protected]", "Israel Aéce"); MailAddress para = new MailAddress("[email protected]", "Israel Aéce"); MailMessage msg = new MailMessage(de, para); msg.Subject = "Teste de E-mail"; string body = @"<img src=""cid:Imagem1"" /><br><br><b>E-mail enviado via .NET 2.0</b>"; AlternateView view = AlternateView.CreateAlternateViewFromString(body, null, MediaTypeNames.Text.Html); LinkedResource resource = new LinkedResource("Logo.gif"); resource.ContentId = "Imagem1"; view.LinkedResources.Add(resource); msg.AlternateViews.Add(view);

Page 279: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 7

Israel Aece | http://www.projetando.net

7

Como podemos analisar no código acima, criamos uma classe do tipo MailMessage, como já fazíamos nas versões anteriores. Dentro do conteúdo do e-mail (body), definimos a tag img e o atributo src que corresponderá a imagem no local que desejarmos. Através do cdi especificamos que o conteúdo será "substituído" pelo conteúdo que mais tarde vamos vir a embutir. Através do método estático CreateAlternateViewFromString, onde passamos o corpo da mensagem e o tipo que ela irá ser (no caso HTML), devolvemos uma instancia da classe AlternateView baseada nesses mesmos parâmetros. Depois disso, criamos um objeto do tipo LinkedResource, onde vamos definir a imagem (ou recurso) que vamos embutir. É importante dizer que a propriedade ContentId deve ter exatamente o mesmo ID que definimos no cid do corpo da mensagem. Agora basta adicionarmos o objeto na coleção de LinkedResources do objeto AlternateView e, este por sua vez, adicionarmos na coleção de Views do objeto MailMessage. A imagem abaixo ilustra o e-mail, dentro do Microsoft Outlook, já com a imagem embutida:

Imagem 11.2 – E-mail com imagem embutida.

A classe SmtpClient Depois da mensagem criada, é necessário enviá-la para o seu destino. A classe MailMessage não tem funcionalidade para isso e, neste momento, utilizaremos a classe SmtpClient. Essa classe permite enviar e-mails através do protocolo SMTP. Para que o envio seja possível, é necessário que você informe os seguintes dados:

• O host que é o servidor SMTP que você utilizará para enviar o e-mail. Essa informação pode ser definida no construtor da classe SmtpClient ou através da propriedade Host.

Page 280: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 8

Israel Aece | http://www.projetando.net

8

• Credenciais para autenticação, se requerida, podendo ser configurada através da propriedade Credentials, também da classe SmtpClient.

• Endereço do remetente, destinatário(s) e o conteúdo a ser enviado. Tudo isso é definido na classe MailMessage, que vimos acima como configurá-la.

A configuração dessa classe pode ser realizada de duas formas: via código ou declarativamente, através do arquivo *.config da aplicação. Basicamente, a diferença é que a segunda opção te fornece uma flexibilidade maior, já que as informações não ficam em hard-code. Para exemplificar, vamos analisar as duas formas, a começar pela configuração via código: VB.NET Imports System.Net.Mail Dim msg As New MailMessage() ‘configuração do MailMessage suprimido Dim smtp As New SmtpClient("mail.servidor.com.br") smtp.Send(msg) C# using System.Net.Mail; MailMessage msg = new MailMessage(); //configuração do MailMessage suprimido SmtpClient smtp = new SmtpClient("mail.servidor.com.br") smtp.Send(msg); Caso o servidor de SMTP necessite de autenticação, então é necessário criar uma instância da classe NetworkCredential, contida no namespace System.Net, informando o userName e o password e, em seguida, atribuir a instância desta classe na propriedade Credentials da classe SmtpClient. A classe SmtpClient utiliza o método Send, passando uma instância de uma classe MailMessage para enviar. A classe SmtpClient ainda permite o envio assíncrono de e-mail, ou seja, ela fornece um método chamado SendAsync que, podemos trabalhar em conjuto com o evento SendCompleted que será disparado quando o envio do e-mail for completado. Esse evento utiliza o delegate SendCompletedEventHandler que define como argumento um objeto do tipo AsyncCompletedEventArgs, que retorna informações a respeito do processo de envio do e-mail. Ambas classes estão contidas dentro do namespace System.ComponentModel. O trecho de código abaixo ilustra como proceder para enviar o e-mail de forma assíncrona:

Page 281: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 9

Israel Aece | http://www.projetando.net

9

VB.NET Imports System.ComponentModel Imports System.Net.Mail Dim msg As New MailMessage() ‘configuração do MailMessage suprimido Dim smtp As New SmtpClient("mail.servidor.com.br") AddHandler smtp.SendCompleted, AddressOf Callback smtp.SendAsync(msg, Nothing) ‘... Public Sub Callback(ByVal sender As Object, _ ByVal e As AsyncCompletedEventArgs) If Not IsNothing(e.Error) Then Console.WriteLine(e.Error.Message) End If End Sub C# using System.ComponentModel; using System.Net.Mail; MailMessage msg = new MailMessage(); //configuração do MailMessage suprimido SmtpClient smtp = new SmtpClient("mail.servidor.com.br") smtp.SendCompleted += new SendCompletedEventHandler(Callback); smtp.SendAsync(msg, null); //... private void Callback(object sender, AsyncCompletedEventArgs e) { if (e.Error != null) { Console.WriteLine(e.Error.Message); } } O segundo parâmetro (definido como Nothing no exemplo) que é passado para o método SendAsync é um objeto do tipo System.Object que será devolvido dentro do método de Callback, através da propriedade UserState do objeto AsyncCompletedEventArgs. Tratamento de Erros

Page 282: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 10

Israel Aece | http://www.projetando.net

10

Quando a classe SmtpClient não consegue, por algum motivo, enviar o e-mail, algumas exceções específicas podem ser atiradas. Ainda dentro do namespace System.Net.Mail, temos algumas exceções que, como já sabemos, herdam direta ou indiretamente da classe Exception, quais são atiradas quando algum problema ocorrece. Para entendermos a hierarquia das exceções dentro deste namespace, vamos analisar a imagem abaixo:

Imagem 11.3 – Hierarquia das exceções do namespace System.Net.Mail.

Abaixo está a descrição para cada uma das exceções: System.Net.Mail.SmtpException: representa uma exceção que é atirada pela classe SmtpClient quando não é possível completar a operação de envio, invocado pelo método Send ou SendAsync. A propriedade StatusCode contém o código do status, retornado pelo servidor de SMTP. System.Net.Mail.SmtpFailedRecipientException: representa uma exceção que é atirada pela classe SmtpClient quando não é possível completar a operação de envio para um destinatário específico, invocado pelo método Send ou SendAsync. System.Net.Mail.SmtpFailedRecipientsException: representa uma exceção que é atirada pela classe SmtpClient quando não é possível entregar a mensagem para todos os destinatários. Como pode ocorrer erros durante o envio de e-mails, é necessário envolver a chamada do método Send ou o método SendAsync em um bloco Try/Catch para capturar a falha e não corromper o seu código. É importante lembrar que a ordem dos blocos “Catchs” devem ser ordenados do mais específico para o mais genérico, que é exatamente a ordem de baixo para cima da imagem 11.3. Com isso conseguimos customizar a mensagem de erro para o usuário e tomar uma decisão mais compatível com o problema ocorrido. O código abaixo exemplifica o uso:

Page 283: Livro de .NET - Israel Aece

Capítulo 10 – Envio de Mensagens (E-mails) 11

Israel Aece | http://www.projetando.net

11

VB.NET Imports System.Net.Mail Try Dim msg As New MailMessage() ‘configuração do MailMessage suprimido Dim smtp As New SmtpClient("mail.servidor.com.br") smtp.Send(msg) Catch e As SmtpFailedRecipientsException Console.WriteLine(e.ToString()) Catch e As SmtpFailedRecipientException Console.WriteLine(e.ToString()) Catch e As SmtpException Console.WriteLine(e.ToString()) Catch e As Exception Console.WriteLine(e.ToString()) End Try C# using System.Net.Mail; try { MailMessage msg = new MailMessage(); //configuração do MailMessage suprimido SmtpClient smtp = new SmtpClient("mail.servidor.com.br") smtp.Send(msg); } catch(SmtpFailedRecipientsException e) { Console.WriteLine(e.ToString()); } catch(SmtpFailedRecipientException e) { Console.WriteLine(e.ToString()); } catch(SmtpException e) { Console.WriteLine(e.ToString()); } catch(Exception e) { Console.WriteLine(e.ToString()); }

Page 284: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 1

Israel Aece | http://www.projetando.net

1

Capítulo 11 Criando Serviços do Windows Introdução Há situações onde precisamos ter sistemas que rodam independentemente da interação de um usuário. Deixar a aplicação a cargo do usuário executá-la de tempo em tempo podemos ter um problema mais sério, já que o usuário pode esquecer de executá-la e, conseqüentemente, uma determinada tarefa deixa de ser executada. Os serviços do Windows (Windows Services) permitem executarmos uma tarefa de forma automática, sem a intervenção humana e sem a necessidade de ter um usuário logado na máquina, podendo inclusive serem inicializados automaticamente quando o sistema operacional entrar no ar. Esses serviços são executados em background, sem que o usuário perceba que o processo está em execução e são ideais para a construção de serviços que exigem um processo de longa duração ou mesmo tarefas periódicas. Esses serviços não possuem interface gráfica, apenas ferramentas do próprio Windows (ou customizadas também através do .NET) para que você possa interagir com o mesmo. Uma das finalidades desta capítulo é apresentar o namespace System.ServiceProcess (contido dentro do Assembly System.ServiceProcess.dll), que fornece as classes e tipos necessários para a construção destes serviços. Além disso, iremos aprender como proceder para a construção, depuração e instalação de um serviço do Windows. Service Control Manager – SCM Uma serviço Windows não pode ser simplesmente executado. Ele precisa de um ambiente para isso, que o execute de forma automática e segura. É neste momento que entra em cena o Service Control Manager – SCM. O SCM permite-nos interagir com um serviço do Windows, ou seja, podemos inicializar, parar e até mesmo executadr comandos em serviços contidos em uma determinada máquina. Além da interface gráfica que o Windows fornece para manipularmos os serviços, dentro do .NET Framework também temos uma classe chamada de ServiceController que permite, via código, interagirmos com os serviços. Criando um serviço do Windows O Visual Studio .NET fornece uma template de projeto, chamada de Windows Service, que podemos utilizar para a criação de um serviço do Windows. Ao criar esse tipo de projeto através do Visual Studio .NET, uma serviço padrão é adicionado ao projeto e, um método chamado Main é criado. Esse método é o ponto de entrada das aplicações .NET e, no caso do serviço do Windows, tem a finalidade que de inicializar o serviço através do método estático Run da classe ServiceBase.

Page 285: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 2

Israel Aece | http://www.projetando.net

2

O Assembly gerado por esse tipo de projeto será do EXE, podendo conter dentro dele vários serviços. Se repararmos, cada serviço que é criado dentro do projeto, herda diretamente de uma classe base chamada ServiceBase. Essa classe é a base para todos os serviços que serão hospedados dentro do Windows. A classe ServiceBase possui uma porção de membros importantes que merecem serem descritos. Através das tabelas abaixo, podemos analisar as principais propriedades e métodos, repectivamente: Propriedade Descrição AutoLog Esta propriedade recebe um valor booleano indicando

se os comandos Start, Stop, Pause e Continue serão logados dentro do Event Log do Windows.

CanHandlePowerEvent Recebe um valor booleano indicando se o serviço poderá ou não receber notificações de mudança do status de energia.

CanHandleSessionChangeEvent Recebe um valor booleano indicando se o serviço pode capturar eventos de mudança de sessão de um Terminal Server.

CanPauseAndContinue Valor booleano que indica se o serviço pode ser pausado e, em seguida, continuado.

CanShutdown Valor booleano que indca se o serviço deve ser notificado quando o sistema operacional estiver em processo de shutting down.

CanStop Indica se o serviço poderá ser parado. EventLog Recebe uma instância da classe EventLog que o

serviço do Windows utilizará para escrever as notificações de Start, Stop, Pause e Continue. Será utilizado a seção Application do Event Log para armazenar essas informações.

ExitCode Define um valor inteiro que é utilizado para reportar algum erro para o SCM.

ServiceName Recebe uma string contendo o nome que identificará o serviço para o Service Control Manager (SCM).

Método Descrição OnContinue Quando implementado na classe derivada, esse método é

executado quando o comando Continue é enviado pelo SCM para o serviço. Geralmente ocorre quando você opta por continuar a execução de um serviço que está atualmente pausado. É importante dizer que se a propriedade CanPauseAndContinue estiver definida com False, o SCM nunca notificará o serviço e, conseqüentemente, o método OnContinue nunca será invocado.

Page 286: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 3

Israel Aece | http://www.projetando.net

3

OnCustomCommand Quando implementado na classe derivada, esse método é executado quando o SCM passa um comando customizado para o serviço, permitindo assim, especificar uma funcionalidade adicional ao serviço. Comandos customizados são passado a partir do método ExecuteCommand da classe ServiceController, qual será abordada mais tarde, ainda neste capítulo.

OnPause Quando implementado na classe derivada, esse método é executado quando o comando Pause é enviado pelo SCM para o serviço. É importante dizer que se a propriedade CanPauseAndContinue estiver definida com False, o SCM nunca notificará o serviço e, conseqüentemente, o método OnPause nunca será invocado.

OnPowerEvent Quando implementado na classe derivada, esse método é executado quando o status de energia é alterado. Isso geralmente é aplicado a notebooks. É importante dizer que se a propriedade CanHandlePowerEvent estiver definida com False, o SCM nunca notificará o serviço e, conseqüentemente, o método OnPowerEvent nunca será invocado.

OnSessionChange Quando implementado na classe derivada, esse método é executado quando o evento é recebido através de uma sessão de Terminal Server. É importante dizer que se a propriedade CanHandleSessionChangeEvent estiver definida com False, o SCM nunca notificará o serviço e, conseqüentemente, o método OnSessionChange nunca será invocado.

OnShutdown Quando implementado na classe derivada, esse método é executado quando o sistema está em processo de shutting down. É importante dizer que se a propriedade CanShutdown estiver definida com False, o SCM nunca notificará o serviço e, conseqüentemente, o método OnShutdown nunca será invocado.

OnStart Quando implementado na classe derivada, esse método é executado quando o comando Start é enviado pelo SCM para o serviço ou quando o sistema operacional é inicializado (desde que o serviço esteja definido para executar automaticamente). A implementação desse método é sempre esperado para que o serviço seja útil e tenha um funcionamento adequado.

OnStop Quando implementado na classe derivada, esse método é

Page 287: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 4

Israel Aece | http://www.projetando.net

4

executado quando o comando Stop é enviado pelo SCM para o serviço. Quando a propriedade CanStop é definida como False, o SCM ignora o comando Stop e, conseqüentemente, não o passa para o serviço. A implementação desse método é sempre esperado para que o serviço seja útil e tenha um funcionamento adequado.

Run (estático) Trata-se de um método estático que tem dois overloads. Um deles recebe uma instância de um objeto do tipo ServiceBase; já o segundo overload, recebe um array de elementos do tipo ServiceBase. Geralmente esse método é chamado dentro do método Main da aplicação Windows Service para servir como ponto de inicialização para o(s) serviço(s). Em seguida, esse mesmo método carrega o(s) serviço(s) para a memória e somente inicializará efetivamente o(s) serviço(s) quando o comando Start for passado pelo SCM.

Vimos que podemos inicializar, pausar ou parar um determinado serviço. Mas como devemos proceder para automatizar as tarefas dentro dos serviços? Exemplo: queremos que ele gerencie um determinado recurso ou periódicamente execute alguma tarefa? Agora, tudo é por conta dos desenvolvedores, ou seja, chegou o momento de escrevermos código dentro do serviço. Esse código terá todo o processo para a execução das tarefas que ele deverá desempenhar. Quando precisamos periódicamente executar uma ou várias tarefas, é muito comum utilizarmos dentro dos serviços do Windows o objeto Timer, que está contido dentro do namespace System.Timers. Esse objeto, dado um intervalo (em milisegundos), ele executa infinitamente (ou até o serviço ser parado) e, quando o intervalo é alcançado, um evento chamado Elapsed é disparado, que é exatamente onde você deverá colocar o código a ser executado. Além disso, em outros cenários, podemos utilizar watchs, como é o caso da objeto FileSystemWatcher, contido dentro do namespace System.IO. Esse objeto monitora um determinado local físico do sistema de arquivos e, quando alguma mudança acontecer, ele detecta e dispara um evento que, você pode utilizar para efetuar algum processamento. Para maiores detalhes sobre este objeto, consulte o Capítulo 5 – Manipulando o sistema de arquivos. Retomando o projeto de Windows Service, temos para cada serviço do Windows dois arquivos relacionados. Eles compõem uma única classe, ambos os arquivos contém uma mesma classe, mas utilizando o conceito das partial classes que, depois de compilado, transforma-se em apenas uma única classe. A tabela abaixo descreve a finalidade de cada um dos arquivos:

Page 288: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 5

Israel Aece | http://www.projetando.net

5

Arquivo Descrição Service1.Designer.vb Service1.Designer.cs

Este arquivo contém a herança da classe base ServiceBase. Além disso, possui todas as informações a nível de inicialização do serviço e também uma parte visual (já que herda indiretamente da classe Component, contido no namespace System.ComponentModel), onde você pode arrastar controles (componentes) para ele.

Service1.vb Service1.cs

Este arquivo, é onde implementamos os códigos de cada um dos eventos que vimos acima.

Para exemplificarmos, a vamos criar um serviço chamado PeopleServices.Service1, onde iremos adicionar um objeto do tipo Timer para escrevermos a hora atual em um arquivo a cada dez segundos (10000 milisegundos). Para manter a utilidade das partial classes, criaremos o objeto Timer dentro do arquivo (classe) Service1.Designer e lá também iremos criar a instância da mesma. Não devemos esquecer de vincular o procedimento que será executado quando o evento Elapsed for disparado e, para isso, utilizaremos o conceito de vinculação dinâmica de eventos. O Timer possui dois métodos chamados Start e Stop que, são auto-explicativos. Quando o serviço do Windows for inicializado, devemos iniciar o Timer e, quando o serviço for parado, devemos parar o Timer. Sendo assim, chamaremos cada um desses métodos nos métodos OnStart e OnStop do serviço do Windows e, dentro do evento Elapsed, o código que executa a escrita no arquivo texto será executado. O código abaixo mostra como efetuar essa configuração que acabamos de descrever nos dois arquivos, apenas poupando algumas seções por questões de espaço: VB.NET – Service1.Designer.vb Imports System.Timers Imports System.ServiceProcess Partial Class Service1 Inherits ServiceBase 'outros membros ocultados Private components As System.ComponentModel.IContainer Private _timer As Timer Private Sub InitializeComponent() components = New System.ComponentModel.Container() Me.ServiceName = "PeopleServices.Service1" Me._timer = New Timers.Timer(10000) AddHandler _timer.Elapsed, AddressOf Me.OnTimedEvent End Sub End Class

Page 289: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 6

Israel Aece | http://www.projetando.net

6

C# - Service1.Designer.cs using System.Timers; using System.ServiceProcess; partial class Service1 : ServiceBase { private System.ComponentModel.IContainer components = null; private Timer _timer; //outros membros ocultados private void InitializeComponent() { components = new System.ComponentModel.Container(); this.ServiceName = "Service1"; this._timer = new Timer(10000); this._timer.Elapsed += new ElapsedEventHandler(OnTimedEvent); } } VB.NET – Service1.vb Imports System.IO Imports System.Timers Public Class Service1 Protected Overrides Sub OnStart(ByVal args() As String) Me._timer.Start() End Sub Private Sub OnTimedEvent(ByVal source As Object, ByVal e As ElapsedEventArgs) Using sw As New StreamWriter(File.Open("C:\Logs.txt", FileMode.Append)) sw.WriteLine(DateTime.Now.ToString("HH:mm:ss")) End Using End Sub Protected Overrides Sub OnStop() Me._timer.Stop() End Sub End Class C# - Service1.cs using System; using System.IO;

Page 290: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 7

Israel Aece | http://www.projetando.net

7

using System.Timers; public partial class Service1 { //outros membros ocultados protected override void OnStart(string[] args) { this._timer.Start(); } void OnTimedEvent(object sender, ElapsedEventArgs e) { using (StreamWriter sw = new StreamWriter(File.Open("C:\\Logs.txt", FileMode.Append))) { sw.WriteLine(DateTime.Now.ToString("HH:mm:ss")); } } protected override void OnStop() { this._timer.Stop(); } } Quando o método Start do objeto Timer é chamado, ele começa a ser executado e, quando o valor especificado no construtor desta classe, que é o intervalo, for atingido, o evento Elapsed será disparado e, no exemplo acima, escreve uma linha no arquivo especificado, com a hora atual. Isso acontecerá até que o método Stop seja invocado. Instalando um serviço do Windows Assim como o .NET Framework trouxe classes para facilitar no desenvolvimento de serviços do Windows, ele também dispõe classes que encapsulam o processo de instalação deste serviço. Ainda dentro do namespace System.ServiceProcess existem duas classes para isso: ServiceInstaller e ServiceProcessInstaller. A primeira delas, ServiceInstaller, é responsável por instalar um serviço que implementa a classe ServiceBase e é chamada pelo utilitário de instalação quando o serviço está sendo instalado. Essa classe herda indiretamente da classe Installer, que vimos extensivamente a sua estrutura no Capítulo 3 – Utilização de Assemblies. Para cada serviço a ser instalado, uma instância desta classe é necessária para efetuar as configurações relacionada a cada um deles. A tabela abaixo sumariza as propriedades mais importantes, descrevendo cada uma delas:

Page 291: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 8

Israel Aece | http://www.projetando.net

8

Propriedade Descrição Description Recebe uma string contendo a descrição para o serviço. Trata-se de uma

descrição para que o usuário que irá operar o serviço saiba para qual finalidade o mesmo se destina. Essa mensagem também exibida na console de gerenciamento fornecida pelo Windows e também através do utilitário de linha de comando, chamado Sc.exe.

DisplayName Através de uma string, define um nome amigável que identifica o nome

do serviço para o usuário. ServiceName Indica o nome uso pelo sistema para identificar o serviço. O valor desta

propriedade deve obrigatoriamente ser identica a propriedade ServiceName da classe ServiceBase do serviço que você quer instalar.

StartType Essa propriedade indica como o serviço será iniciado. Ela recebe uma das opções fornecidas pelo enumerador ServiceStartMode, que pode ser uma das três opções descritas abaixo:

• Automatic – Indica que o serviço será inicializado automaticamente quando o sistema operacional for inicializado.

• Disabled – Indica que o serviço está desabilitado e não pode ser inicializado pelo usuário ou pela aplicação.

• Manual – Indica que o serviço pode somente ser inicializado manualmente, por um usuário ou por uma aplicação.

Page 292: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 9

Israel Aece | http://www.projetando.net

9

Já a classe ServiceProcessInstaller é responsável por instalar um executável que contém classes que estendem a classe ServiceBase e é também chamada pelo utilitário de instalação. Através da instância da classe ServiceProcessInstaller podemos (e devemos) definir informações relacionadas a parte de segurança do serviço. Temos três propriedades que utilizamos para definir a segurança, a saber: Propriedade Descrição Account Indica qual o tipo de conta em que o serviço irá rodar. Essa propriedade

é definida através do enumerador ServiceAccount, que possui quatro opções, descritas abaixo:

• LocalService – Irá executar no contexto de uma conta que age como um usuário não privilegiado no computador local e com credenciais anônimas em qualquer computador remoto.

• LocalSystem – Irá executar no contexto de uma conta que possue privilégios locais e possui credenciais em qualquer computador remoto.

• NetworkService – Irá executar no contexto de uma conta que age sem privilégios no computador local e possue credenciais em qualquer computador remoto.

• User – Opção padrão. Irá solicitar um nome de usuário e senha válidos para quando o serviço é instalado e executado no contexto de uma conta especificada na rede. As informações de nome de usuário e senha são informados através das propriedades UserName e Password, que estão logo abaixo.

Password Define uma string contendo a senha associada com a conta de usuário em que o serviço irá ser executado.

UserName Define uma string contendo o nome de usuário em que o serviço irá ser executado.

Nota: A definição das propriedades UserName e Password são importante, pois automatizam a inicialização do serviço quando o sistema operacional, por algum motivo, for reinicializado e o serviço precisa ser inicializado. Quando a propriedade Account estiver definida como User e as propriedades UserName e Password estiverem vazias, ao instalar o serviço, será necessário que você informe o nome de usuário e senha e, se não forem válidos, o serviço não será instalado. Felizmente o Visual Studio .NET facilita a criação destas duas classes necessárias para a instalação dos serviços. Quando você abre o arquivo do serviço em modo design, ao clicar com o botão direito do mouse em qualquer parte dele, abrirá uma menu de contexto e terá uma opção chamada Add Installer. Ao clicar nela, um arquivo chamando ProjectInstaller é adicionado ao projeto e, dentro dele, as classes que vimos acima, já pré-configuradas com as propriedades que vimos acima, podendo ser altereadas também

Page 293: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 10

Israel Aece | http://www.projetando.net

10

através da janela de propriedades. O ProjectInstaller nada mais é que uma classe que herda diretamente da classe Installer e que está decorada com o atributo RunInstallerAttribute que, como vimos no Capítulo 3, indica que o instalador será executado quando você instalar o Assembly. Os serviços Windows, depois de devidamente criados e com seus respectivos instaladores, chega o momento onde devemos colocá-lo em seu devido lugar dentro do sistema operacional e, para isso, utilizamos o utilitário chamado installutil.exe que, dado um caminho físico até o Assembly (EXE), ele sai a procura de classes que estão decoradas com o atributo RunInstallerAttribute definido com True e as executam. Depois disso, se tudo ocorrer com sucesso, você já conseguirá visualizar o serviço dentro da console do Windows, como é mostrado na imagem abaixo:

Imagem 12.2 – Serviço da People já devidamente instalado.

Interagindo com um serviço do Windows Depois de devidamente instalado, é necessário termos o controle sobre ele, ou seja, precisamos ter acesso as operações básicas de todo serviço Windows, que é iniciar, parar, pausar, recuperar informações, etc.. Para isso, temos três formas. A primeira delas, é utilizando a console que o Windows fornece que está em Painel de Controle, Ferramentas Administrativas e, sem seguida, clique em Serviços; outra possibilidade é através de um utilitário de linha de comando chamado Sc.exe; finalmente, temos a possibilidade de controlar um serviço do Windows a partir de uma aplicação .NET e, para isso, temos uma classe chamada ServiceController. Essa classe representa um determinado serviço do Windows e

Page 294: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 11

Israel Aece | http://www.projetando.net

11

fornece todas as funcionalidades necessárias para que possamos manipular o serviço que ela representa. Quando utilizamos a console fornecida pelo próprio sistema operacional, temos a possibilidade de localizar o serviço e, quando clicamos com o botão direito do mouse em cima do mesmo, um menu de contexto é exibido com as operações que podemos executar no serviço, como por exemplo, o método Start, Stop e Pause. A imagem abaixo ilustra esse menu:

Imagem 12.3 – Console fornecida pelo Windows para gerenciamento do serviço.

Além dela, temos também o utilitário de linha de comando, chamado Sc.exe. O utilitário serve para quando queremos automatizar a manipulação do serviço, ou seja, a partir de uma instalação de um software, necessitamos parar um determinado serviço e, quando a instalação for concluída, o serviço é reinicializado. A imagem abaixo exibe como executar esse utilitário e passar os comandos para que ele possa trabalhar:

Page 295: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 12

Israel Aece | http://www.projetando.net

12

Imagem 12.4 – Utilitário de linha de comando para a manipulação de serviços do Windows.

Finalmente, temos uma solução .NET para acessar um determinado serviço do Windows. Trata-se de uma classe chamada ServiceController. Essa classe fornece o acesso a um serviço do Windows, possibilitando operá-lo e extrair informações sobre ele. O interessante é que, como os dois primeiros casos, não está limitado a acessar serviços somente feitos em .NET, mas podendo acessar todo e qualquer serviço Windows. Além disso, ele fornece uma funcionalidade interessante que não temos na console do Windows: é possível criar uma aplicação .NET que envie comandos customizados para o serviço através desta classe. Essa classe também é fornecida como um controle, contida na aba componentes do Visual Studio .NET, como é mostrado na imagem abaixo:

Imagem 12.5 – Controle ServiceController na ToolBox do Visual Studio .NET.

Através da tabela abaixo, analisaremos os membros mais importantes que são fornecidos pela classe ServiceController: Membro Descrição CanPauseAndContinue Propriedade de somente leitura que retorna um valor booleano

indicando se o serviço referenciado permite ou não ser pausado e, em seguida, continuado.

Page 296: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 13

Israel Aece | http://www.projetando.net

13

CanShutdown Propriedade de somente leitura que retorna um valor booleano indicando se o serviço referenciado permite ou não ser notificado quando o sistema operacional estiver em processo de shutting down.

CanStop Propriedade de somente leitura que retorna um valor booleano indicando se o serviço referenciado permite ou ser parado depois de inicializado.

DependentServices Recupera um array de elementos do tipo ServiceController, onde cada elemento representa um serviço que depende do serviço referenciado.

DisplayName Retorna uma string contendo o nome amigável do serviço referenciado.

MachineName Propriedade que podemos definir ou ler o nome da máquina em que o serviço reside. O padrão é o computador local, também indicado como “.”.

ServiceName Propriedade que podemos definir ou ler o serviço que a instância do classe ServiceController referencia.

ServicesDependendOn Recupera um array de elementos do tipo ServiceController, onde cada um dos elementos representa um serviço que o serviço referenciado depende.

ServiceType Define um dos valores estipulados pelo enumerador ServiceType, que são descritos abaixo:

• Adapter – Um serviço para um dispositivo de hardware que requer seu próprio driver.

• FileSystemDriver – Um driver do sistema de arquivos, que também pode ser um driver de dispositivo de Kernel.

• InteractiveProcess – Um serviço que pode interagir com o Desktop.

• KernelDriver – Um driver de dispositivo de Kernel, como disco rígido ou qualquer outro driver de mais baixo nível.

• RecognizerDriver – Um driver de sistema de arquivo que é utilizado durante a inicialização para determinar o sistema de arquivo presente no sistema.

• Win32OwnProcess – Um programa que pode ser iniciado pelo ServiceController e que obedece o protocolo do serviço.

• Win32SharedProcess – Um serviço Win32 que pode compartilhar o processo com outros serviços Win32.

Status Propriedade de somente leitura que retorna o status atual do serviço referenciado, que é definido pelo enumerador ServiceControllerStatus, que tem as seguintes opções:

Page 297: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 14

Israel Aece | http://www.projetando.net

14

• ContinuePending – O serviço continua pendente. • Pause – O serviço está pausado. • PausePending – O serviço está sendo pausado. • Running – O serviço está rodando. • StartPending – O serviço está sendo iniciado. • Stopped – O serviço está parado. • StopPending – O serviço está sendo parado.

É importante analisar que somente terá o status mais atual depois que chamar o método Refresh, que veremos mais abaixo.

Close Este método desconecta a instância do classe ServiceController do serviço e libera todos os recursos relacionados a ele.

Continue Responsável por continuar um serviço, depois dele estar pausado.

ExecuteCommand Através deste método é possível passar um comando customizado para o serviço sem alterar o seus status. Neste caso, o ideal é sempre implementar o método OnCustomCommand para, dentro do serviço, recuperar essa informação e tomar uma decisão baseando-se nisso.

GetDevices Trata-se de um método estático que recupera todos os serviços relacionados a driver de dispositivos no computador referenciado. Esse método retorna um array contendo elementos do tipo ServiceController.

GetServices Trata-se de um método estático que recupera todos os serviço do computador referenciado. Esse método retorna um array contendo elementos do tipo ServiceController.

Pause Pausa o serviço. Refresh Atualiza para os valores correntes todas os valores das

propriedades, como é o caso da propriedade Status. Se esse método não for chamado, você nunca conseguirá recuperar o status mais atual em que o serviço se encontra.

Start Inicializa o serviço. Stop Para o serviço e qualquer serviço que é dependente do mesmo. WaitForStatus Dado uma opção contida dentro do enumerador

ServiceControllerStatus, aguarda o serviço atingir este status. Esse método é sobrecarregado, que permite receber um objeto do tipo TimeSpan, para que seja possível especificar um timeout, pois se caso o status nunca for atingido, é possível determinar um tempo e, se ele for atingido e o status não for mudado, ele interromperá a espera.

Page 298: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 15

Israel Aece | http://www.projetando.net

15

Para dar um exemplo simples da utilização da classe ServiceController, vamos recuperar o serviço People que criamos um pouco acima e invocar algumas das propriedades: VB.NET Imports System.ServiceProcess Dim sc As New ServiceController("PeopleServices.Service1", ".") lblNome.Text = "DisplayName: " & sc.DisplayName lblServiceName.Text = "ServiceName: " & sc.ServiceName C# using System.ServiceProcess; ServiceController sc = new ServiceController("PeopleServices.Service1", "."); lblNome.Text = "DisplayName: " + sc.DisplayName; lblServiceName.Text = "ServiceName: " + sc.ServiceName; É importante salientar que as propriedades DisplayName e ServiceName diferem bastante. A propriedade DisplayName é apenas um nome amigável que damos ao serviço para que os usuários que estejam visualizando consigam identificar melhor o serviço. Já a propriedade ServiceName é utilizado para identificar o serviço para o sistema operacional e é que você precisa informar no construtor da classe ServiceController para que seja possível recuperá-lo. Depurando um serviço do Windows Como os serviços do Windows devem ser executados a partir do SCM, é necessário instalarmos antes de executá-lo, mesmo que essa execução seja ainda em tempo de desenvolvimento, mais precisamente, em depuração. Além de instalado, para que a depuração funcione, é necessário que ele também esteja inicializado. O processo de depuração é um pouco mais detalhado em relação as aplicações de interface gráfica. O primeiro passo importante é certificar-se de que está instalando um Assembly em seu modo Debug. Depois do serviço devidamente instalado, você pode abrir o projeto do serviço dentro do Visual Studio .NET, marcar onde irá querer o BreakPoint e, em seguida, vá até o menu Debug >> Attach To Process (ou através do atalho Ctrl + Alt + P) e na caixa de diálogo que lhe é apresentada, selecione o serviço na lista de serviços em execução e então clique no botão Attach. Isso fará com que o debugger do Visual Studio .NET seja anexado ao processo do serviço e, quando a execução chegar no Breakpoint, você conseguirá acompanhar o processo via linha de código. A imagem abaixo ilustra a janela que é apresentada para você escolher o processo qual quer anexar o depurador:

Page 299: Livro de .NET - Israel Aece

Capítulo 11 – Criando Serviços do Windows 16

Israel Aece | http://www.projetando.net

16

Imagem 12.6 – Anexando o debugger ao serviço Windows.

Page 300: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 1

Israel Aece | http://www.projetando.net

1

Capítulo 12 Interoperabilidade com componentes COM Introdução Antes mesmo da plataforma .NET surgir, as empresas já estavam desenvolvendo aplicações e componentes nas mais diversas linguagens, como por exemplo C++, Visual Basic 6, etc.. Não há dúvidas que as empresas investiram tempo e dinheiro na criação destes componentes que são, em algumas vezes, largamente utilizados e, dificilmente, serão sobrescritos instantaneamento para .NET, utlizando o código gerenciado. Não somente esse cenário, mas temos diversos outros componentes que são expostos via COM que poderíamos estar utilizando dentro da plataforma .NET. Felizmente o .NET tem a capacidade de comunicar com o mundo COM, permitindo criarmos componentes que sejam acessíveis a linguagens não .NET e, também, permite utilizarmos os componentes legados dentro das aplicações .NET. Na primeira parte deste capítulo, vamos abordar como devemos proceder para a criação de componentes que serão utilizados por aplicações/componentes COM e também como consumir componentes COM. A segunda, e última parte, destina-se a analisarmos um serviço chamado PInvoke que a plataforma .NET nos disponibiliza. Ela nos permite fazer chamadas a DLLs não gerenciadas e, além disso, efetuarmos chamadas as APIs que o Windows disponibiliza para termos acesso a informações do sistema operacional. Acessando componentes COM Criação de Assembly para Interoperabilidade Antes de mais nada, vamos entender o que é algo “interoperável”: “Interoperabilidade é a capacidade de um sistema ( informatizado ou não) de se comunicar de forma transparente (ou o mais próximo disso) com outro sistema (semelhante ou não). Para um sistema ser considerado interoperável, é muito importante que ele trabalhe com padrões abertos. Seja um sistema de portal, seja um sistema educacional ou ainda um sistema de comércio eletrônico, ou e-commerce, hoje em dia se caminha cada vez mais para a criação de padrões para sistemas.”

Wikipédia

Como o .NET já é uma realidade, muitas empresas já estão trabalhando em cima dessa plataforma, mas mantendo alguns softwares e componentes ainda desenvolvidos em uma tecnologia mais antiga. Hoje encontramos muitas aplicações rodando em Visual Basic 6 e, como já discutimos acima, foi injetado muitos recursos para o desenvolvimento dessas aplicações, que não podem ser descartados.

Page 301: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 2

Israel Aece | http://www.projetando.net

2

Quando começamos a falar em migração, se o software foi “politicamente escrito”, então a parte da interface com o usuário, geralmente, é onde temos menos códigos, o que conseqüentemente será a primeira parte a ser migrada para a nova plataforma. Sendo assim, mesmo em .NET, queremos continuar utilizando os componentes feitos em Visual Basic 6.0 que, neste primeiro momento, não serão migrados. Para tornar a comunicação entre o mundo COM e o mundo .NET possível, entra em cena um Assembly chamado Interop Assembly. Esse Assembly, fornece uma layer intermediária entre os dois mundos, que habilita a comunicação entre o runtime .NET Framework e o componente (ou aplicação) COM. O Interop Assembly nada mais é que um wrapper para os membros (metadados) que o mesmo expõe. Para gerar esse Assembly de interoperabilidade, basta você adicionar uma referência a um componente COM em uma aplicação .NET. Ao compilar a aplicação, dentro do diretório \bin da mesma você notará que, além do Assembly que é gerado pela aplicação, um outro Assembly, este de interoperabilidade, também é gerado com a seguinte nomenclatura: Interop.[Nome Componente COM].dll. E esses Interop Assemblies são criados um para cada referência COM que tiver em seu projeto. Se você não tiver o Visual Studio .NET no momento, então você pode recorrer a um utilitário de linha de comando, qual também tem a finalidade de criar Interop Assemblies. Esse utilitário é chamado TlbImp.exe e é encontrado dentro do diretório onde está contido o .NET Framework. Uma dica aqui é abrir o utilitário a partir do prompt do Visual Studio .NET para evitar procurar o path completo do mesmo. Uma possível sintaxe para utilizar este utilitário é: C:\ tlbimp Componente.dll /out:Interop.Componente.dll Além dessas duas forma, ainda há a possibilidade de criar esse componente dinamicamente, ou seja, o .NET Framework fornece classes que permitem, via código, criarmos um Interop Assembly. Para isso, existe um namespace chamado System.Runtime.InteropServices que fornece todos os tipos necessários para implementarmos essa solução. A principal classe que temos dentro deste namespace que fornece o conjunto de serviços necessários para converter um Assembly gerenciado acessível via COM e extrair um Interop Assembly de um componente COM é a classe TypeLibConverter que, inclui as mesmas opções do utilitário Tlbimp.exe. Essa classe possui três métodos que podemos utilizar, dependendo da necessidade. Esses métodos são: Método Descrição ConvertAssemblyToTypeLib Dado um Assembly .NET, extrai a biblioteca de tipos e

cria um Assembly que pode ser utilizado através do mundo COM.

Page 302: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 3

Israel Aece | http://www.projetando.net

3

ConvertTypeLibToAssembly Dado um Assembly COM, extrai a biblioteca de tipos e cria um Assembly de interoperabilidade que pode ser utilizado por aplicações .NET.

GetPrimaryInteropAssembly Através de uma GUID, que identifica o objeto dentro (type library) do Registry, recupera o nome e o code base de um Assembly de interoperabilidade. Esse método retorna um valor booleano indicando se Assembly foi ou não encontrado.

Expondo componentes .NET para o mundo COM Uma vez que criamos classes em .NET aplicações COM podem utilizá-las, se assim desejar. Entretanto, para assegurar que isso seja possível, é necessário nos atentarmos em alguns detalhes que precisam ser analisados para que o mesmo consiga ser visível ao mundo COM. Para disponibilizar o componente para interoperabilidade com o mundo COM, você primeiramente deve desenhar o seu componente visando facilitar esse processo e, para isso, você deve explicitamente implementar Interfaces em seus componentes que serão expostos. Isso é necessário porque componentes COM “não podem conter” os membros diretamente e, sendo assim, devem ter as Interfaces implementadas. Apesar do COM poder gerar a Interface automaticamente, é melhor você criar isso manualmente, o que permitirá um maior controle sob o componente e suas formas de atualização. Quando os componentes COM geram automaticamente a sua Interface, você não pode fazer nenhum mudança em sua estrutura pública. Isso acontece porque componentes COM são imutáveis. Se você romper essa regra, o componente deixará de ser invocado. Justamente por essa questão que permitir que a Interface seja criada automaticamente não é uma boa idéia e, o ideal é estar criando as suas próprias Interfaces, pois você terá uma maior segurança, já que conseguirá garantir que a Interface não mudará. Para manipularmos a forma com a qual a Interface é criada e manipulada, temos um atributo chamado ClassInterfaceAttribute que devemos decorar o componente. Esse atributo recebe em seu construtor, uma das opções do enumerador ClassInterfaceType, que estão descritas abaixo: Opção Descrição AutoDispatch Opção padrão que indica que a classe suporta late-binding quando

chamada através de um cliente COM e omite a descrição da Interface. Utilize essa opção quando a classe pode sofrer mudanças futuras.

AutoDual Esta opção cria uma Interface que permite o early-binding. Somente utilize essa opção se essa classe não sofrerá mudanças futuras.

None O COM Interop não criará a Interface padrão para a classe. Neste caso, você deverá, explicitamente, fornecer uma Interface para ser implementada na classe que será exposta para o mundo COM.

Page 303: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 4

Israel Aece | http://www.projetando.net

4

O segundo detalhe que temos que nos atentar é com relação aos membros que desejamos expor ao mundo COM. Por padrão, construtores parametrizados, métodos estáticos e constantes não são expostos. Com exceção disso, todos os tipos e membros públicos são acessíveis via COM. Mas e se existir algum tipo ou membro público que você quer ocultar dos clientes COM? É neste caso que entra em cena o atributo ComVisibleAttribute. Esse atributo pode ser utilizado nos mais diversos tipos e membros e, em seu construtor, recebe um parâmetro booleano indicando se o membro será ou não visível para os clientes COM. O código abaixo exemplifica a implementação de uma Interface em uma classe. Esse processo é idêntico ao que já conhecemos dentro da plataforma .NET. Em seguida, aplicamos o atributo ClassInterfaceAttribute no componente, definindo em seu construtor a opção None do enumerador ClassInterfaceType. Finalmente, uma propriedade é criada mas negamos o acesso a mesma para clientes COM através do atributo ComVisibleAttribute, definindo-o como False: VB.NET Imports System.Runtime.InteropServices Public Interface IMensagens Function BoasVindas(ByVal nome As String) As String End Interface <ClassInterface(ClassInterfaceType.None)> _ Public Class Usuarios Implements IMensagens Public Function BoasVindas(ByVal nome As String) As String _ Implements IMensagens.BoasVindas Return "Olá " & nome End Function <ComVisible(False)> _ Public ReadOnly Property SenhaTemporaria() As String Get Return "P@$$w0rd" End Get End Property End Class C# using System.Runtime.InteropServices; public interface IMensagens { string BoasVindas(string nome);

Page 304: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 5

Israel Aece | http://www.projetando.net

5

} [ClassInterface(ClassInterfaceType.None)] public class Usuarios : IMensagens { public string BoasVindas(string nome) { return "Olá " + nome; } [ComVisible(false)] public string SenhaTemporaria { get { return "P@$$w0rd"; } } } Um detalhe importante é com relação ao atributo ComVisibleAttribute. Por padrão, esse atributo é definido como True e, sendo assim, todos os tipos são gerenciados estão disponíveis para o mundo COM. Esse atributo, além de decorar individualmente cada tipo, pode também ser aplicado a nível de Assembly, através do arquivo Assembly.vb ou Assembly.cs. Depois do Assembly .NET devidamente gerado, chega o momento de utilizá-lo em uma aplicação COM, como por exemplo, o Visual Basic 6. Mas somente com o Assembly .NET “puro” ainda não é possível, pois o mesmo precisa ser primeiramente registrado para, somente assim, ser acessado através do aplicações COM. Para registrá-lo, existe um utilitário de linha de comando chamado regasm.exe, que está contido dentro da seguinte pasta: %windir%\Microsoft.NET\Framework\v2.0.50727. Esse utilitário lê os metadados do Assembly, os extrai e adiciona as entradas necessárias dentro do Registry, permitindo assim, o acesso via clientes COM. Abaixo um exemplo de como registrar um componente .NET com esse utilitário: C:\ regasm Componente.dll /tlb:Componente.tlb Uma vez feito isso, basta você adicionar a referência ao mesmo em seu projeto (COM) e utilitá-lo. Definindo a Interface padrão

Page 305: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 6

Israel Aece | http://www.projetando.net

6

Quando desejamos expor um componente .NET para o mundo COM, ele deve ter algumas configurações extras em relação aos componentes que são utilizados somente por aplicações .NET. Quando o definimos com o valor ClassInterfaceType.None no componente, indicará que a criação da Interface será fornecida por nós. Neste caso, ao registrar o componente com o Type Library Exporter (Tlbexp.exe), o mesmo será exposto com a primeira interface pública visível encontrada pelo utilitário, definindo assim, a interface padrão do componente para o mundo COM. Mas e quando existerem mais que uma Interface implementada no componente e, por algum motivo, queremos expor não a primeira Interface pública visível, mas a segunda ou a terceira. A versão 2.0 do .NET Framework introduz um novo atributo chamado ComDefaultInterfaceAttribute que, podemos expecificá-lo no componente para determinar qual será a interface padrão utilizada independentemente da ordem de implementação. Em seu construtor, devemos especificar a Interface (através de um objeto do tipo Type) padrão para o mundo COM. O exemplo abaixo exemplifica o componente decorado com este atributo: VB.NET Imports System.Runtime.InteropServices <ClassInterface(ClassInterfaceType.None)> _ <ComDefaultInterface(GetType(ILogin))> _ Public Class AuthenticationServices Implements IData Implements ILogin ‘Implementação... End Class C# using System.Runtime.InteropServices; [ClassInterface(ClassInterfaceType.None)] [ComDefaultInterface(typeof(ILogin))] public class AuthenticationServices : IData, ILogin { //Implementação... } Para consumir o componente no mundo COM, podemos fazer (exemplo em VB6): [ Sem o atributo ComDefaultInterface ] Dim authService As New AuthenticationServices

Page 306: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 7

Israel Aece | http://www.projetando.net

7

Dim login As ILogin Set login = authService authService.Update() MsgBox login.Validate("IA", "Pa$$w0rd") [ Com o atributo ComDefaultInterface ] Dim authService As New AuthenticationServices Dim data As IData Set data = authService MsgBox authService.Validate("IA", "Pa$$w0rd") data.Update() PInvoke O .NET Framework fornece uma serviço chamado PInvoke – Platform Invoke. Esse serviço possibilita a chamada a código não gerenciado, que estão implementados em DLL (dynamic link library), como é o caso das APIs do Windows. Esse serviço localiza e invoca uma função e se encarrega de efetuar o marshaling dos argumentos automaticamente quando é necessário. Marshaling é a técnica utilizada para converter valores e trafegá-los entre processos e threads diferentes. As técnicas de marshaling são utilizadas para compatibilidade de tipos. Quando invocamos um método não gerenciado a partir do código gerenciado, os tipos de dados devem ser convertidos em um tipo correspondente do mundo não gerenciado e, quando não é possível mapear tipos apenas com a declaração de tipos correspondentes, temos que utilizar técnicas diferentes como conversão de formato e o comportamento dos mesmos. Veremos mais a respeito do mashalling no decorrer deste capítulo. A imagem abaixo ilustra o processo de chamada a um código gerenciado a partir do PInvoke:

Imagem 13.1 – Processo de chamada do PInvoke.

Page 307: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 8

Israel Aece | http://www.projetando.net

8

O processo ocorre em quatro passos:

1. Localiza a DLL contendo a função. 2. Carrega a DLL na memória. 3. Localiza a função dentro da memória, passando os parâmetros e efetuando o

marshaling quando necessário. 4. Transfere o controle para a função do código não gerenciado. 5. Se exceções acontecerem elas são atiradas pela função de código não gerenciado

para o chamador do código gerenciado. Observação: Localizar, carregar a DLL e localizar a função na memória somente ocorre na primeira chamada para a função. Para chamar uma função de uma DLL que está escrita em código não gerenciado a partir do código gerenciado, primeiramente devemos criar uma definição desta função em código gerenciado, dentro de uma classe qualquer. Essa função não tem nenhuma implementação de código e obrigatoriamente deverá conter a mesma assinatura da função não gerenciada que deseja invocar. Depois disso, utilizamos um atributo chamado DllImportAttribute que é utilizado para especificar a localização da DLL onde está contido o método externo. Observação 1: Existe um site chamada PInvoke.net: http://www.pinvoke.net que fornece gratuitamente uma listagem, já com exemplos de códigos, para as APIs do Win32 e outros componentes não gerenciados, com exemplos em Visual Basic .NET e Visual C#. Observação 2: Antes de utilizar uma API do Windows, verifique se não existe uma classe gerenciada (dentro do .NET Framework) que tenha a mesma finalidade. O atributo DllImporteAttribute possue em seu construtor alguns named parameters que podemos configurá-los para customizar o comportamento do PInvoke. Esses parâmetros são descritos na tabela abaixo: Parâmetro Descrição BestFitMapping Indica se o ajuste durante a conversão de ANSI para

Unicode. Isso permitirá uma análise mais precisa com relação a conversão que, as vezes, pode não ser o ideal, pois podemos perder ou ser substituída alguma informação. Caracteres que não conseguem ser mapeados são representados pelo ponto de interrogação “?”.

CallingConvention Este campo indica qual será a convensão adotada para a chamada do método não gerenciado. Esse campo aceita umas das opções contidas no enumerador CallingConvention. Abaixo estão as descrições das opções

Page 308: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 9

Israel Aece | http://www.projetando.net

9

que ele fornece.

• Cdecl – O chamador limpa a stack. Utilizado para casos em que o método um número variado de parâmetros.

• FastCall – Não suportado. • StdCall – A função limpa a stack. Esta é a

convensão padrão para quando desejar invocar funções não gerenciadas através do PInvoke.

• Winapi – Indica que usará a conversão adotada pelo sistema operacional. Esta também é a opção padrão.

CharSet Indica qual será o conjuto de caracteres utilizados durante o marshaling de strings. Esse valor é definido a partir do enumerador CharSet, que contém as seguintes opções:

• Ansi – Efetua o marshaling de strings em formato ANSI.

• Auto – O PInvoke decide em tempo de execução entre Ansi e Unicode, baseando-se no sistema operacional.

• Unicode – Efetua o marshaling de strings em formato Unicode de 2 bytes.

EntryPoint Indica o nome da função da DLL não gerenciada que será invocada. Esse valor somente é exigido quando o nome da função não gerenciada difere da definição (nome) da função gerenciada.

ExactSpelling Este campo controla como o runtime irá efetuar a busca pela função a ser executada. Se este campo estiver definido como False, e o CharSet estiver definido como Ansi, a letra “A” será adicionada no final do nome da função; agora, se o CharSet estiver definido como Unicode, a letra “W” é adicionada no final do nome da função. Isso porque quando estamos trabalhando com as funções das APIs do Windows, que trabalham com strings, normalmente temos duas versões da mesma função disponíveis, que é a versão Ansi e a versão Unicode, que são sufixadas respectivamente por “A” e “W”. A tabela abaixo exibe um relacionamento entre os campos CharSet e ExactSpelling, exibindo o valor da propriedade ExactSpelling baseando-se nos valores padrões definidos por cada linguagem:

Page 309: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 10

Israel Aece | http://www.projetando.net

10

Linguagem Ansi Unicode Auto VB.NET True True False C# False False False

PreserveSig Indica se os métodos não gerenciados que possuem

HRESULT ou retval como valores de retornos são diretamente “traduzidos” ou convertidos em exceções.

• True – O retorno do método será um número inteiro que conterá o HRESULT.

• False – O método automaticamente converterá os valores de HRESULT ou retval em exceções. É importante se atentar aqui, pois exige que a assinatura do método seja mudada para retornar um número inteiro.

SetLastError Indica se SetLastError é ou não chamado. Se o valor for definido como True, o marshaler invoca GetLastError and faz o cache do valor retornado para previnir que ele seja sobrescrito por alguma outra API. Assim, você poderá recuperar o código de erro através do método estático GetLastWin32Error da classe Marshal.

ThrowOnUnmappableChar Valor booleano que indica se uma exceção será atirada quando um caracter não conseguir ser mapeado.

O código abaixo exemplifica como devemos criar o método gerenciado para a chamada da função não gerenciada e, neste exemplo, iremos invocar a função LogonUser da API advapi32.dll. Esse método retornará uma valor booleano indicando se as credenciais informadas através dos parâmetros userName e password são ou não válidas. VB.NET Imports System Imports System.Runtime.InteropServices Public Class Program <DllImport("advapi32.dll")> _ Public Shared Function LogonUser( _ ByVal lpszUsername As String, _ ByVal lpszDomain As String, _ ByVal lpszPassword As String, _ ByVal dwLogonType As Integer, _ ByVal dwLogonProvider As Integer, _ ByRef phToken As IntPtr) As Boolean End Function Public Shared Sub Main()

Page 310: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 11

Israel Aece | http://www.projetando.net

11

Dim token As IntPtr If LogonUser("Usuario", String.Empty, "P@$$w0rd", 2, 0, token) Then Console.WriteLine("Usuário válido.") Else Console.WriteLine("Usuário inválido.") End If End Sub End Class C# using System; using System.Runtime.InteropServices; class Program { [DllImport("advapi32.dll")] private static extern bool LogonUser( String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken); static void Main(string[] args) { IntPtr token; if (LogonUser("login", string.Empty, "P@$$w0rd", 2, 0, out token)) Console.WriteLine("Usuário válido."); else Console.WriteLine("Usuário inválido."); } } Como podemos notar no código acima, apenas decoramos o método gerenciado LogonUser com o atributo DllImportAttribute, sem definir os demais campos que vimos na tabela acima, o que fará com que ele assuma os valores padrões. É importante dizer que você poderá customizar esses valores de acordo com a sua necessidade, olhando sempre a finalidade para qual cada um deles se destina. Nota Quando estamos utilizando o Visual Basic .NET, ele fornece uma alternativa, mas restrita, para invocar métodos não gerenciados. Para isso, utilizamos uma keyword chamada Declare em conjunto com outra keyword chamada Alias que, por sua vez,

Page 311: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 12

Israel Aece | http://www.projetando.net

12

permite definirmos qual a função do código gerenciado queremos invocar. Abaixo está um exemplo da utilização da função LogonUser da API advapi32.dll com esta forma exclusiva do Visual Basic .NET: Imports System Imports System.Runtime.InteropServices Public Class Program Public Declare Auto Function LogonUser Lib "advapi32.dll" Alias "LogonUser" ( _ ByVal lpszUsername As String, _ ByVal lpszDomain As String, _ ByVal lpszPassword As String, _ ByVal dwLogonType As Integer, _ ByVal dwLogonProvider As Integer, _ ByRef phToken As IntPtr) As Boolean Public Shared Sub Main() Dim token As IntPtr If LogonUser("Israel Aece", String.Empty, "P@$$w0rd", 2, 0, token) Then Console.WriteLine("Usuário válido.") Else Console.WriteLine("Usuário inválido.") End If End Sub End Class

Como podemos notar, a função deve receber os mesmos parâmetros da função não gerenciada e, a forma de acesso a mesma dentro do método Main é exatamente a mesma. Para finalizar, essa forma de acesso é interessante, porém possui apenas um subconjunto de configurações para a chamada da função não gerenciada. Se desejar ter uma maior controle nas configurações, utilize o atribute DllImportAttribute. Passagem de Parâmetros Como comentado acima, quando invocamos uma função de código não gerenciado, os parâmetros e seus valores de retorno (quando existem) devem ser convertidos em parâmetros correspondentes ao mundo não gerenciado. Esse processo, como já sabemos, chama-se mashalling. A maioria dos tipos de dados da plataforma .NET são convertidos sem nenhum problema para o mundo gerenciado, pois há sempre um tipo correspondente. Se desejar visualizar uma tabela completa com o mapeamente de tipos entre código gerenciado e não

Page 312: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 13

Israel Aece | http://www.projetando.net

13

gerenciado, consulte este link: http://msdn2.microsoft.com/en-us/library/ac7ay120.aspx (MSDN Library). As exceções são para tipos genéricos, estruturas e objetos. Tipos genéricos não são suportados e estruturas ou objetos exigem informações adicionais para essa conversão. Geralmente o CLR controla o layout físico dos campos de uma estrutura ou objeto dentro da memória gerenciada. Se essa estrutura ou classe precisa ser ajustado de alguma forma, você deve decorar a estrutura ou objeto com o atributo StructLayoutAttribute. Esse atributo permite você controlar explicitamente o layout físico dos campos de uma estrutura ou objeto que serão passados para o código não gerenciado que, por sua vez, espera em um layout específico. Esse atributo fornece um construtor sobrecarregado que aceita como parâmetro uma das opções fornecidas pelo enumerador LayoutKind. As opções fornecidas por esse enumerador estão descritas logo abaixo: Opção Descrição Auto O runtime automaticamente escolhe um layout apropriado para os

membros de um determinado objeto dentro da memória não gerenciada. Explicit Especifica que cada de cada membro dentro da memória não gerenciada

é explicitamente controlada. Para isso, cada membro deve ser marcada com o atributo FieldOffsetAttribute para indicar a posição do campo dentro do tipo.

Sequential Indica que os membros de um determinado objeto serão expostos de forma seqüencial, na ordem em que eles aparecem quando eles são exportados para a memória não gerenciada.

Abaixo é exibido um exemplo de como utilizar este atributo: VB.NET Imports System Imports System.Runtime.InteropServices <StructLayout(LayoutKind.Sequential)> _ Public Structure Point Public X As Integer Public Y As Integer End Structure C# using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)] public struct Point { public int X; public int Y;

Page 313: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 14

Israel Aece | http://www.projetando.net

14

} Apesar do marshaling ser um processo que acontece de forma transparente para os demais tipos de dados, há a possibilidade de customizarmos essa conversão para que problemas com incompatibilidade de tipos não aconteça. Para essa customização, temos a disposição o atributo MarshalAsAttribute que permite você mudar o comportamento padrão da conversão de tipos (campos, parâmetros ou valores de retornos) e especificar um tipo mais específico. No entanto esse atributo somente é necessário quando um tipo pode ser convertido em vários outros tipos. Um exemplo disso é a conversão de uma string em código não gerenciado. Ela pode ser convertida em vários tipos: LPStr, LPWStr, LPTStr ou Bstr. Por padrão, ela é convertida em Bstr para métodos COM. O atributo MarshalAsAttribute possui um construtor sobrecarregado que aceita uma das opções fornecidas pelo enumerador UnmanagedType. Esse enumerador determinada como os tipos serão convertido para o código não gerenciado. Suas opções são os tipos de dados não gerenciados disponíveis que podemos escolher para mapear um tipo gerenciado em um tipo não gerenciado, mudando assim o comportamento padrão. O código abaixo mostra um exemplo do uso deste atributo em um parâmetro de método e em um campo público: VB.NET Imports System Imports System.Runtime.InteropServices <DllImport("Componente.dll")> _ Private Shared Function FuncaoTeste(<MarshalAs(UnmanagedType.LPStr)> ByVal s As String) As Boolean End Function ‘---- <MarshalAs(UnmanagedType.LPStr)> _ Public Silly As Nome C# using System; using System.Runtime.InteropServices; [DllImport("Componente.dll"] private static extern bool FuncaoTeste([MarshalAs(UnmanagedType.LPStr)] string s)

Page 314: Livro de .NET - Israel Aece

Capítulo 12 – Interoperabilidade com componentes COM 15

Israel Aece | http://www.projetando.net

15

//---- [MarshalAs(UnmanagedType.LPStr)] public string Nome; A classe Marshal A classe Marshal fornece uma coleção de estáticos métodos para interagir e manipular o código não gerenciado. Esses metódos estáticos fornecidos pela classe Marshal são essenciais para trabalhar com código não gerenciado. Muitos desses métodos que estão definidos nesta classe são tipicamente utilizados por desenvolvedores que precisam fornecer/criar uma ponte entre os mundos gerenciados e não gerenciados.

Page 315: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 1

Israel Aece | http://www.projetando.net

1

Capítulo 13 Reflection Introdução Reflection (ou Reflexão) é a habilidade que temos em extrair e descobrir informações de metadados de um determinado Assembly. Os metadados descrevem os campos (propriedades, membros e eventos) de um tipo juntamente com seus métodos e, durante a compilação, o compilar gerará e armazenará metadados dentro do Assembly. São os metadados que permitem uma das maiores façanhas dentro da plataforma .NET, ou seja, escrevermos um componente em Visual C# e consumí-lo em uma aplicação em Visual Basic .NET. Reflection não permite apenas extrair informações em runtime, mas também permitirá que se carreegar Assemblies, instancie classes, invoque seus métodos, etc.. Reflection é algo muito poderoso que existe e possibilita dar uma grande flexibilidade para a aplicação. O póprio .NET Framework utiliza Reflection internamente em diversos cenários, como por exemplo o Garbage Collector examina os objetos que um determinado objeto referencia para saber se o mesmo está ou não sendo utilizado. Além disso, quando serializamos um objeto, o .NET Framework utiliza Reflection para extrair todos os valores do membros internos do objeto para persistí-los. O próprio Visual Studio .NET utiliza informações extraídas via Reflection para habilitar o Intellisense e mais, quando está desenvolvendo um formalário e vai até a janela de propriedades de um determinado controle, o Visual Studio .NET extrai os membros do controle via Reflection para exibir e, conseqüentemente, alterar de acordo com a necessidade. A idéia deste capítulo é apresentar como utilizar essa ferramenta poderosa que a Microsoft disponibilizou dentro do .NET Framework para extrair informações de metadados. Todas as classes que utilizaremos para Reflection estão contidas dentro do namespace System.Reflection e, na primeira parte do capítulo, veremos como é possível carregar Assemblies em memória, para em seguida, conseguir extrair as informações de classes, propriedades, métodos e eventos que um determinado tipo apresenta. AppDomains Historicamente, um processo é criado para isolar uma aplicação que está sendo executado dentro do mesmo computador. Cada aplicação é carregada dentro de um processo separado, que isola uma aplicação das outras. Esse isolamento é necessário porque os endereços são relativos à um determinado processo. O código gerenciado passa por um processo de verificação que deve ser executado antes de rodar. Esse processo determina se o código pode acessar endereço de memória inválidos ou executar alguma outra ação que poderia causar alguma falha no processo. O código que passa por essa verificação é chamado de type-safe e permite ao CLR fornecer um grande nível de isolamento, como um processo, só que com muito mais performance.

Page 316: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 2

Israel Aece | http://www.projetando.net

2

AppDomain ou domínio de aplicação é um ambiente isolado, seguro e versátil que o CLR pode utilizar para isolar aplicações. Você pode rodar várias aplicações em um único processo Win32 com o mesmo nível de isolamento que existiria em um processo separado para cada uma dessas aplicações, sem ter o overhead que existe quando é necessário fazer uma chamado entre esses processos. Além disso, um AppDomain é seguro, já que o CLR garante que um AppDomain não conseguirá acessar os dados que estão contidos em outro AppDomain. Essa habilidade de poder rodar várias aplicações dentro de um único processo aumenta consideravelmente a escalabilidade do servidor. Um AppDomain é criado para servir de container para uma aplicação gerenciada. A inicialização de um AppDomain consiste em algumas tarefas, como por exemplo, a criação da memória heap, onde todos os reference-types são alocados e de onde o lixo é coletado e a criação de um pool de threads, que pode ser utilizado por qualquer um dos tipos gerenciados que estão carregados dentro do processo. O Windows não fornece uma forma de rodar aplicação .NET. Isso é feito a partir de uma CLR Host, que é uma aplicação responsável por carregar o CLR dentro de um processo, criando AppDomains dentro do mesmo e executando o código das aplicações que desenvolvemos dentro destes AppDomains. Quando uma aplicação é inicializada, um AppDomain é criado para ela. Esse AppDomain também é conhecido como default domain e ele somente será descarregado quando o processo terminar. Sendo assim, o Assembly inicial rodará dentro deste AppDomain e, se desejar, você pode criar um novos AppDomains e carregar dentro destes outros Assemblies mas, uma vez carregado, você não pode descarregá-lo e isso somente acontecerá quando a AppDomain for descarregada. O .NET Framework disponibiliza uma classe chamada AppDomain que representa e permite manipular AppDomains. Essa classe fornece vários métodos (alguns estáticos) que auxiliam desde a criação até o término de um AppDomain. Entre esses principais métodos, temos: Método Descrição CreateDomain Método estático que permite a criação de uma nova AppDomain. CurrentDomain Retorna um objeto do tipo AppDomain representando o

AppDomain da thread corrente. DoCallback Executa um código em uma outra aplicação a partir de um

delegate. GetAssemblies Retorna um array de objetos do tipo Assembly, onde cada

elemento é um Assembly que foi carregado dentro do AppDomain.

IsDefaultAppDomain Retorna uma valor boolano indicando se o AppDomain trata-se do AppDomain padrão.

Load Permite carregar um determinado Assembly dentro do

Page 317: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 3

Israel Aece | http://www.projetando.net

3

AppDomain. Unload Descarrega um determinado AppDomain. Para exemplificar a criação e o descarregamento de um segundo AppDomain, vamos analisar o trecho código abaixo: VB.NET Imports System Dim _domain As AppDomain = AppDomain.CreateDomain("TempDomain") ‘... AppDomain.Unload(_domain) C# using System; AppDomain _domain = AppDomain.CreateDomain("TempDomain"); //... AppDomain.Unload(_domain); Assemblies Nomenclatura dos Assemblies A nomenclatura de um Assembly (conhecida como display name ou identidade do Assembly) consiste em 4 informações: nome (sem “.exe” ou “.dll”), versão, cultura e a public key token (que será nula se uma strong name não for definida). Para exemplificar isso, vamos analisar o Assembly System.Data.dll da versão 2.0 do .NET Framework que é responsável pelas classes de acesso a dados e também um Assembly customizado chamado PeopleLibrary, onde não foi criado uma strong name: System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 PeopleLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null O .NET Framework fornece uma classe chamada que descreve a identidade do Assembly, chamada AssemblyName. Entre as várias propriedades que essa classe possui, podemos destacar as mais importantes: CodeBase, CultureInfo, FullName, KeyPair, Name e Version. Carregamento manual de Assemblies

Page 318: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 4

Israel Aece | http://www.projetando.net

4

Quando referenciamos um Assembly qualquer dentro de uma aplicação, o CLR decide quando carregá-lo. Quando você chama um método, o CLR verifica o código IL para ver quais tipos estão sendo referenciados e, finalmente, carrega os Assemblies onde os tais tipos estão sendo referenciados. Se um Assembly já estiver contido dentro do AppDomain, o CLR é inteligente ao ponto de conseguir identificar e não carregará o mesmo Assembly novamente. Mas quando você quer extrair informações de metadados de um determinado Assembly, é necessário carregá-lo para dentro do seu AppDomain e, depois disso, poder extrair os tipos que ele expõe. Para que isso seja possível, o namespace System.Reflection possui uma classe chamada Assembly que possui, além de seus métodos de instância, alguns métodos estáticos que são utilizados para carregar um determinado Assembly. Entre esses métodos estáticos para carregamento do Assembly temos: Método Descrição GetAssembly Dado um determinado tipo, esse método consegue extrair o

Assembly em qual esse tipo está contido. GetCallingAssembly Retorna um objeto do tipo Assembly que representa o

Assembly onde o método está sendo invocado. GetEntryAssembly Retorna um objeto do tipo Assembly que está contido no

AppDomain padrão (default domain). GetExecutingAssembly Retorna uma instância do Assembly onde o código está sendo

executado. Load Um método com vários overloads que retorna um

determinado Assembly. Um dos overloads mais comuns, é o que aceita uma string, conteudo o seu fully qualified name (nome, versão, cultura e token) como vimos acima. Se o Assembly especificado conter com uma strong name, o CLR então procura dentro do GAC, seguido pelo diretório base da aplicação e dos diretórios privados da mesma. Agora, se o Assembly especificado não conter uma strong name, ele apenas não procurará dentro do GAC e, para ambos os casos, se o Assembly não for encontrado, uma exceção do tipo System.IO.FileNotFoundException.

LoadFile Permite carregar um Assembly a partir do caminho fiísico até o mesmo, podendo carregar um Assembly de qualquer local que ele esteja, não resolvendo as dependências. Esse método é utilizando em cenários mais limitados, onde não é possível a utilização do método LoadFrom, já que não permite carregar os Assemblies que tenham a mesma identidade, mas estão fisicamente em locais diferentes.

LoadFrom Carrega um Assembly baseando-se no caminho físico,

Page 319: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 5

Israel Aece | http://www.projetando.net

5

resolvendo todas as suas dependências (ver nota abaixo). LoadWithPartialName Carrega um Assembly do diretório da aplicação ou do GAC,

mas trata-se de um método obsoleto e, ao invés dele, utilize o método Load. Esse método não deve ser utilizado, porque você nunca sabe a versão do Assembly que irá carregar.

ReflectionOnlyLoad Carrega um Assembly em um contexto reflection-only, ou seja, o Assembly poderá apenas ser examinado, mas não executado.

ReflectionOnlyLoadFrom Dado o caminho físico até o Assembly, o mesmo é carregado dentro do domínio do chamador, também em um contexto reflection-only.

Nota Importante: Vamos imaginar que temos uma aplicação Windows Forms (EXE) e uma biblioteca (DLL) que essa aplicação Windows utiliza. A estrutura é a seguinte: C:\Program Files\PeopleUI\WinUI.exe C:\Program Files\PeopleUI\PeopleLibrary.dll

Como podemos ver, a aplicação Windows (WinUI.exe) foi desenvolvida referenciando a biblioteca PeopleLibrary.dll. Imagine que, dentro do método Main ele carrega a seguinte DLL: “C:\PeopleLibrary.dll” através do método LoadFrom da classe Assembly. Imagine que, depois de carregado o mesmo Assembly, mas de um outro local, ao invocar um método dentro do PeopleLibrary.dll ele invocará de qual dos dois locais (“C:\” ou “C:\Program Files\PeopleUI\”)? Como o CLR não pode supor que os Assemblies são iguais somente porque o nome do arquivo coincide, felizmente ele sabe qual deverá executar. Quando o método LoadFrom é executado, o CLR extrai informações sobre a identidade do Assembly (versão, cultura e token), passando essas informações para o método Load que, faz a busca pelo Assembly. Se um Assembly correspondente for encontrado, o CLR fará a comparação do caminho do arquivo especifico no método LoadFrom e do caminho encontrado pelo método Load. Se os caminhos forem idênticos, o Assembly será considerado parte da aplicação, caso contrário, será considerado um “arquivo de dados”. Sempre que possível, é bom evitar o uso do método LoadFrom e opte por utilizar o método Load. A razão para isso é que, internamente, o método LoadFrom invoca o método Load. Além disso, o LoadFrom trata o Assembly como um “arquivo de dados” e, se o CLR carrega dentro de um AppDomain o mesmo Assembly a partir de caminhos diferentes, uma grande quantidade de memória é desperdiçada.

Page 320: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 6

Israel Aece | http://www.projetando.net

6

Exemplo: O código abaixo exemplifica a utilização do método LoadFrom da classe Assembly. O trecho de código foi extraído de uma demonstração e, você pode consultar na íntegra no seguinte local: XXXXXXXXXXXXXXXXXXXXXX. VB.NET Imports System Imports System.Reflection Dim asb As Assembly = Assembly.LoadFrom("C:\Comp.dll") C# using System; using System.Reflection; Assembly asb = Assembly.LoadFrom("C:\\Comp.dll"); Trabalhando com Metadados Como já vimos anteriormente, os metadados são muito importantes dentro da plataforma .NET. Vimos também como criar AppDomains e carregar Assemblies dentro deles. Somente carregar os Assemblies dentro de um AppDomain não tem muitas utilidades. Uma vez que ele encontra-se carregado, é perfeitamente possível extrair informações de metadados dos tipos que estão contidos dentro Assembly e, para isso, utilizaremos várias classes que estão contidas dentro do namespace System.Reflection. A partir de agora analisaremos essas classes que estão disponíveis para a criação e manipulação de tipos, como por exemplo, invocar métodos, recuperar ou definir valores para propriedades, etc.. Antes de mais nada, precisamos entender qual a hierarquia dos objetos que estão disponíveis para a manipulação dos metadados e para invocá-los dinamicamente. A imagem abaixo exibe tal hierarquia onde, como já era de se esperar, o ancestral comum é o System.Object.

Page 321: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 7

Israel Aece | http://www.projetando.net

7

Imagem 15.1 – Hierarquia das classes para manipulação de metadados

System.Reflection.MemberInfo MemberInfo é uma classe abstrata que é base para todas as classes utilizadas para resgatar informações sobre os membros sejam eles construtores, eventos, campos, métodos ou propriedades de uma determinada classe. Basicamente essa classe fornece as funcionalidades básicas para todos as classes que dela derivarem. Os membros desta classe abstrata são: Membro Descrição Name Retorna uma string representando o membro. MemberType Propriedade de somente leitura que retorna um item do

enumerador MemberTypes indicando qual tipo de membro ele é. Entre os itens deste enumerador, temos:

• All – Especifica todos os tipos • Constructor – Especifica que o membro é um construtor,

representado pelo tipo ConstructorInfo. • Custom – Especifica que o membro é um membro

customimzado.

Page 322: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 8

Israel Aece | http://www.projetando.net

8

• Event – Especifica que o membro é um evento, representado pelo tipo EventInfo.

• Field – Especifica que o membro é um campo, representado pelo tipo FieldInfo.

• Method – Especifica que o membro é um método, representado pelo tipo MethodInfo.

• NestedType – Especifica que o membro é um tipo aninhado, representado pelo tipo MemberInfo.

• Property – Especifica que o membro é uma propriedade, representada pelo tipo PropertyInfo.

• TypeInfo – Especifica que o membro é um tipo, representado pelo tipo TypeInfo.

DeclaringType Propriedade de somente leitura que retorna um objeto do tipo Type indicando de qual tipo é o objeto.

ReflectedType Propriedade de somente leitura que retorna um objeto do tipo Type indicando o tipo que foi utilizado para obter a instância deste membro.

GetCustomAttributes Retorna um array contendo algum atributos customizados que o membro contém. Cada um dos elementos é representado por um System.Object.

IsDefined Retorna um valor booleano indicando se existe ou não um determinado atributo especifico aplicado no membro.

System.Type Essa é uma das mais importantes classes da plataforma .NET. Essa é uma das principais classes utilizadas para você poder extrair informações de um tipo. System.Object possui um método chamado GetType que retorna o tipo da instância corrente, sendo representado por um objeto do tipo Type. Sendo assim, todo e qualquer objeto possui esse método, já que todo tipo herda direta ou indiretamente dessa classe. O método GetType acima utilizamos quando já temos a instância do objeto. Mas e quando não a temos? Para esse caso, a classe Type fornece um método estático, também chamado de GetType que, dado um tipo (através de uma string contendo o AssemblyQualifiedName, que inclui o namespace até o nome do tipo que deve ser carregado), ele retorna o objeto Type que o representa e, conseqüentemente, conseguirá extrair as informações de metadados do mesmo. Além desse método, a classe Type ainda possui um método interessante chamado GetArrayType, que retorna um array de objetos do tipo Type, onde cada elemento representa o tipo correspondente do elemento do array. Entre todos os membros da classe Type, podemos destacar: Membro Descrição Assembly Propriedade de somente leitura que retorna um objeto do

tipo Assembly em que o tipo é declarado. BaseType Propriedade de somente leitura que retorna um objeto do

Page 323: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 9

Israel Aece | http://www.projetando.net

9

tipo Type que representa o tipo qual o tipo corrente foi herdado.

DeclaringType Propriedade de somente leitura que retorna um objeto do tipo Type que representa o tipo do membro.

IsAbstract Propriedade de somente leitura que retorna um valor booleano indicando se o tipo é ou não abstrato.

IsArray Propriedade de somente leitura que retorna um valor booleano indicando se o tipo é ou não um array.

IsByRef Propriedade de somente leitura que retorna um valor booleano indicando se o tipo é ou não passado por referência.

IsClass Propriedade de somente leitura que retorna um valor booleano indicando se o tipo é ou não uma classe.

IsEnum Propriedade de somente leitura que retorna um valor booleano indicando se o tipo é ou não um enumerador.

IsGenericParameter Propriedade de somente leitura que retorna um valor booleano indicando se o tipo representa um type parameter de um tipo genérico ou uma definição de método genérico.

IsGenericType Propriedade de somente leitura que retorna um valor booleano indicando se o tipo é ou não um tipo genérico.

IsInterface Propriedade de somente leitura que retorna um valor booleano indicando se o tipo é ou não uma Interface.

IsNested Propriedade de somente leitura que retorna um valor booleano indicando se o tipo é ou não aninhado a outro tipo.

IsValueType Propriedade de somente leitura que retorna um valor booleano indicando se o tipo é ou não um tipo-valor.

FindInterfaces Retorna um array de objetos do tipo Type com todas as Interfaces implementadas ou herdadas pelo tipo.

FindMembers Retorna um array de objetos do tipo MemberInfo com todos os membros de um determinado tipo.

GetConstructor Método sobrecarregado que procura por um construtor de instância público. A busca é feita baseando-se em um array de objetos do tipo Type que é passado para esse método, que procurará pelo construtor que atender exatamente esses parâmetros. Se encontrado, uma instância da classe ConstrutorInfo é retornada.

GetConstructors Retorna um array de objetos do tipo ConstructorInfo, onde cada elemento representa um construtor do tipo.

GetCustomAttributes Retorna um array de objetos do tipo System.Object, onde cada elemento representa um atributo que foi aplicado ao membro.

GetDefaultMembers Procura por membros que aplicam o atributo DefaultMemberAttribute. Se encontrado, um array de elementos do tipo MemberInfo é retornado, onde cada

Page 324: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 10

Israel Aece | http://www.projetando.net

10

elemento representará um membro que aplica o atributo acima especificado. O atributo DefaultMemberAttribute define um determinado membro como sendo um membro padrão, que é invocado pelo método InvokeMember da classe Type.

GetEvent Dado uma string com o nome de um evento existente no tipo, esse método retorna um objeto do tipo EventInfo representando o evento.

GetEvents Retorna um array de objetos do tipo EventInfo, onde cada elemento representa um evento existente no tipo.

GetField Dado uma string com o nome de um campo existente no tipo, esse método retorna um objeto do tipo FieldInfo representando o campo.

GetFields Retorna um array de objetos do tipo FieldInfo, onde cada elemento representa um campo público existente no tipo.

GetInterface Dado uma string com o nome de uma Interface, ele retornará um objeto do tipo Type que represente a mesma, desde que ela esteja implementada no tipo.

GetInterfaces Retorna um array de objetos do tipo Type com todas as Interfaces implementadas ou herdadas pelo tipo.

GetMember Dado uma string com o nome de um membro existente no tipo, esse método retorna um objeto do tipo MemberInfo representando o membro.

GetMembers Retorna um array de objetos do tipo MemberInfo, onde cada elemento representa um membro (propriedades, métodos, campos e eventos) existente no tipo.

GetMethod Dado uma string com o nome de um método existente no tipo, esse método retorna um objeto do tipo MethodInfo representando o método.

GetMethods Retorna um array de objetos do tipo MethodInfo, onde cada elemento representa um método existente no tipo.

GetNestedType Dado uma string com o nome de um tipo aninhado existente no tipo, esse método retorna um objeto do tipo Type representando o tipo aninhado.

GetNestedTypes Retorna um array de objetos do tipo Tipo, onde cada elemento representa um tipo aninhado existente no tipo.

GetProperties Retorna um array de objetos do tipo PropertyInfo, onde cada elemento representa uma propriedade existente no tipo.

GetProperty Dado uma string com o nome de uma propriedade existente no tipo, esse método retorna um objeto do tipo PropertyInfo representando a propriedade.

Page 325: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 11

Israel Aece | http://www.projetando.net

11

Como temos duas formas de extrair o Type de algum objeto, o código abaixo exemplifica ambas, aplicando a lógica em cima, a primeira delas é utilizando o método GetType a partir da instância da classe e a outra forma é através do método estático GetType da classe Type. VB.NET Imports System Dim id As Integer = 123 Dim forma1 As Type = id.GetType() Dim forma2 As Type = Type.GetType("System.Int32") Console.WriteLine(forma1.FullName) Console.WriteLine(forma2.FullName) C# using System; int id = 123; Type forma1 = id.GetType(); Type forma2 = Type.GetType("System.Int32"); Console.WriteLine(forma1.FullName); Console.WriteLine(forma2.FullName); Ambos os códigos exibirão System.Int32, que representa o FullName do tipo inteiro. Vimos acima todos os métodos que a classe Type fornece. Cada um dos métodos retornam objetos específicos para cada um dos membros que existem dentro de um determinada objeto. A partir daqui, como já somos capazes de extrair o tipo de um objeto, iremos analisar esses objetos específicos, responsáveis por representar os membros do tipo, onde poderemos extrair informações de metadados referentes a cada um deles. Mas para que podemos entender o grande potencial do Reflection, vamos criar uma classe customizada, que possua membros, propriedades, métodos e eventos para que possamos extrair as informações de metadados da mesma. Isso não quer dizer que não seja possível extrair as mesmas informações de uma classe de dentro do .NET Framework. Para fins de exemplo, vamos criar uma classe chamada Cliente que será composta por alguns membros que iremos utilizar como base para os futuros exemplos: VB.NET Public Class Cliente Public X As Integer Private _id As Integer Private _nome As String

Page 326: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 12

Israel Aece | http://www.projetando.net

12

Public Event AlterouDados As EventHandler Public Sub New() End Sub Public Sub New(ByVal id As Integer, ByVal nome As String) Me._id = id Me._nome = nome End Sub Public Property Id() As Integer Get Return Me._id End Get Set(ByVal value As Integer) Me._id = value RaiseEvent AlterouDados(Me, EventArgs.Empty) End Set End Property Public Property Nome() As String Get Return Me._nome End Get Set(ByVal value As String) Me._nome = value RaiseEvent AlterouDados(Me, EventArgs.Empty) End Set End Property Public Function ExibeDados() As String Dim msg As String = String.Format("{0} - {1}", Id, Nome) Return msg End Function End Class C# public class Cliente { public int X; private int _id; private string _nome; public event EventHandler AlterouDados; public Cliente() { } public Cliente(int id, string nome) { this._id = id; this._nome = nome;

Page 327: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 13

Israel Aece | http://www.projetando.net

13

} public int Id { get { return _id; } set { _id = value; if (AlterouDados != null) AlterouDados(this, EventArgs.Empty); } } public string Nome { get { return _nome; } set { _nome = value; if (AlterouDados != null) AlterouDados(this, EventArgs.Empty); } } public string ExibeDados() { string msg = string.Format("{0} - {1}", Id, Nome); return msg; } } Como podemos ver, a classe possui dois membros, um do tipo inteiro e uma string e, para cada um deles, temos uma propriedade de escrita/leitura que encapsulam o acesso aos mesmos e um evento que é disparado quando o valor da propriedade é alterado; além disso, há também dois construtores, onde um deles não possui nenhum parâmetro e o outro recebe o numero e nome do cliente; finalmente, temos um método chamado ExibeDados que retorna uma string com o código e nome do cliente. Antes de extrairmos as informações relacionados a cada um dos membros internos, precisamos primeiramente recupera o tipo através do método GetType, fornecido pela instância da classe Cliente, que será armazenado em um objeto chamado tipo qual iremos utilizar por todos os exemplos adiante. VB.NET Dim cliente As New Cliente() cliente.Id = 123 cliente.Nome = "José Torres" Dim tipo As Type = cliente.GetType()

Page 328: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 14

Israel Aece | http://www.projetando.net

14

C# Cliente cliente = new Cliente(); cliente.Id = 123; cliente.Nome = "José Torres"; Type tipo = cliente.GetType(); System.Reflection.FieldInfo Essa classe representa um membro público de um determinado tipo, fornecendo informações de metadata e atributos que possam estar aplicados ao membro. Essa classe não possui um construtor público e, instâncias da mesma, são retornadas a partir dos métodos GetField e GetFields da classe Type. Abaixo é exibido a forma que podemos utilizar e extrair os membros públicos da classe Cliente a partir do método GetFields: VB.NET Imports System.Reflection For Each fi As FieldInfo In tipo.GetFields() Console.WriteLine( _ String.Format("Nome: {0} - Tipo: {1}", _ fi.Name, fi.FieldType)) Next C# using System.Reflection; foreach (FieldInfo fi in tipo.GetFields()) { Console.WriteLine( string.Format("Nome: {0} - Tipo: {1}", fi.Name, fi.FieldType)); } Output Nome: X - Tipo: System.Int32 A propriedade FieldType retorna um objeto do tipo Type representando o tipo do membro. Além desta propriedades existem algumas outras que merecem ser comentadas, como por exemplo, IsPublic, IsPrivate e IsStatic, que são auto-explicativas. System.Reflection.MethodBase

Page 329: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 15

Israel Aece | http://www.projetando.net

15

A classe MethodBase trata-se de uma classe abstrata que fornece informações a respeito de métodos e construtores de um determinado tipo, atributos que são aplicados aos mesmos e métodos e propriedades para manipulação de métodos genéricos. Ela fornece também um método chamado GetParameters, que retorna uma coleção de objetos do tipo ParameterInfo, onde cada um deles representa um parâmetro que pode existir no método. A classe ParameterInfo possui uma propriedade chamada ParameterType que retorna um objeto do tipo Type representando o tipo do parâmetro e, além dela, a classe ParameterInfo possui algumas outras propriedades úteis para extrairmos informações relacionadas a cada parâmetro e, entre elas, temos a propriedade IsOut, que retorna um valor booleano indicando se o parâmetro trata-se de um parâmetro de output. A classe MethodBase ainda possui um método, um tanto quanto interessante, chamado GetMethodBody. Esse método retorna um objeto do tipo MethodBody, contendo o MSIL stream, variáveis locais (declaradas dentro do método) e estruturas de exceções. Finalmente, a classe MethodBase classe serve como base para as classes concretas ConstructorInfo e MethodInfo, que trazem informações customizadas para cada um dos tipos. O primeiro deles, ConstructorInfo, trata-se de uma classe que representa um determinado construtor público de um tipo, fornecendo informações de metadados à respeito do construtor e também descobrindo os atributos que o construtor pode ter. Essa classe não possui um construtor público e, instâncias da mesma, são retornadas a partir dos métodos GetConstructor e GetConstructos da classe Type. O código abaixo exemplifica a utilização da classe ConstructorInfo, extraindo os construtores da classe Cliente: VB.NET Imports System.Reflection For Each ci As ConstructorInfo In tipo.GetConstructors() Console.WriteLine(String.Format("Construtor: {0}", ci.Name)) For Each pi As ParameterInfo In ci.GetParameters() Console.WriteLine( _ String.Format(" Parâmetro: {0} - Tipo: {1}", _ pi.Name, pi.ParameterType)) Next Next C# using System.Reflection; foreach (ConstructorInfo ci in tipo.GetConstructors())

Page 330: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 16

Israel Aece | http://www.projetando.net

16

{ Console.WriteLine(String.Format("Construtor: {0}", ci.Name)); foreach (ParameterInfo pi in ci.GetParameters()) { Console.WriteLine( string.Format(" Parâmetro: {0} - Tipo: {1}", pi.Name, pi.ParameterType)); } } Output Construtor: .ctor Construtor: .ctor Parâmetro: id - Tipo: System.Int32 Parâmetro: nome - Tipo: System.String Como podemos ver, temos um laço For aninhado que é utilizado para recuperar os parâmetros de um determinado construtor a partir de uma instância de um objeto ConstructorInfo. A segunda e última classe que deriva de MethodBase, a classe MethodInfo, tem um comportamento muito parecido com a classe ConstructorInfo, só que neste caso, cada objeto deste representa um método público de um tipo. A classe MethodInfo também pode receber parâmetros e ter atributos. A única exceção é que a classe MethodInfo pode ter um tipo de retorno, ou seja, uma função que retorna um valor qualquer e, para isso, a classe MethodInfo fornece uma propriedade chamado ReturnType que retorna um objeto do tipo Type representando o tipo que o método retorna. Essa classe não possui um construtor público e, instâncias da mesma, são retornadas a partir dos métodos GetMethod e GetMethods da classe Type. Através do código abaixo podemos visualizar a utilização da classe MethodInfo. Vale lembrar que todas as classes herdam direta ou indiretamente da classe System.Object e, conseqüentemente, todos os métodos públicos lá contidos, como por exemplo, GetType, Equals, ToString etc. são também exibidos. VB.NET Imports System.Reflection For Each mi As MethodInfo In tipo.GetMethods() Console.WriteLine( _ String.Format("Método: {0} - Retorno: {1}", _ mi.Name, mi.ReturnType)) For Each pi As ParameterInfo In mi.GetParameters()

Page 331: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 17

Israel Aece | http://www.projetando.net

17

Console.WriteLine( _ String.Format(" Parametro: {0} - Tipo: {1}", _ pi.Name, pi.ParameterType)) Next Next C# using System.Reflection; foreach (MethodInfo mi in tipo.GetMethods()) { Console.WriteLine( String.Format("Método: {0} - Retorno: {1}", mi.Name, mi.ReturnType)); foreach (ParameterInfo pi in mi.GetParameters()) { Console.WriteLine( string.Format(" Parâmetro: {0} - Tipo: {1}", pi.Name, pi.ParameterType)); } } Output Método: add_AlterouDados - Retorno: System.Void Parâmetro: value - Tipo: System.EventHandler Método: remove_AlterouDados - Retorno: System.Void Parâmetro: value - Tipo: System.EventHandler Método: get_Id - Retorno: System.Int32 Método: set_Id - Retorno: System.Void Parâmetro: value - Tipo: System.Int32 Método: get_Nome - Retorno: System.String Método: set_Nome - Retorno: System.Void Parâmetro: value - Tipo: System.String Método: ExibeDados - Retorno: System.String Método: GetType - Retorno: System.Type Método: ToString - Retorno: System.String Método: Equals - Retorno: System.Boolean Parâmetro: obj - Tipo: System.Object Método: GetHashCode - Retorno: System.Int32 System.Reflection.PropertyInfo Essa classe representa uma propriedade pública de um determinado tipo, fornecendo informações de metadata e atributos que possam estar aplicados à propriedade. Essa classe não possui um construtor público e, instâncias da mesma, são retornadas a partir

Page 332: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 18

Israel Aece | http://www.projetando.net

18

dos métodos GetProperty e GetProperties da classe Type. A classe PropertyInfo possui uma propriedade chamada PropertyType, que retorna um objeto do tipo Type representando o tipo da propriedade e ainda, possui duas propriedades chamadas CanRead e CanWrite, que retornam um valor booleano indicando se a propriedade pode ser lida e escrita, respectivamente. Abaixo é exibido a forma que podemos utilizar e extrair os membros públicos da classe Cliente a partir do método GetProperties: VB.NET Imports System.Reflection For Each pi As PropertyInfo In tipo.GetProperties() Console.WriteLine( _ String.Format("Propriedade: {0} - Tipo: {1}", _ pi.Name, pi.PropertyType)) Next C# using System.Reflection; foreach (PropertyInfo pi in tipo.GetProperties()) { Console.WriteLine( string.Format("Propriedade: {0} - Tipo: {1}", pi.Name, pi.PropertyType)); } Output Propriedade: Id - Tipo: System.Int32 Propriedade: Nome - Tipo: System.String System.Reflection.EventInfo Essa classe representa um evento público de um determinado tipo, fornecendo informações de metadata e atributos que possam estar aplicados ao evento. Essa classe não possui um construtor público e, instâncias da mesma, são retornadas a partir dos métodos GetEvent e GetEvents da classe Type. A classe EventInfo possui uma propriedade chamada EventHandlerType, que retorna um objeto do tipo Type representando o tipo do delegate utilizado para declarar o evento. Abaixo é exibido a forma que podemos utilizar e extrair os membros públicos da classe Cliente a partir do método GetEvents: VB.NET Imports System.Reflection

Page 333: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 19

Israel Aece | http://www.projetando.net

19

For Each ei As EventInfo In tipo.GetEvents() Console.WriteLine( _ String.Format("Evento: {0} - Tipo: {1}", _ ei.Name, ei.EventHandlerType)) Next C# using System.Reflection; foreach (EventInfo ei in tipo.GetEvents()) { Console.WriteLine( string.Format("Evento: {0} - Tipo: {1}", ei.Name, ei.EventHandlerType)); } Output Evento: AlterouDados - Tipo: System.EventHandler Invocando os membros em runtime Agora que já sabemos como extrair as informações dos tipos, como podemos fazer para invocar esses membros automaticamente? Mas para fazermos isso, é necessário entendermos o conceito de Binding. Binding é o processo de localizar a implementação de um determinado tipo e existem dois tipos: Early Binding e Late Binding. O Early Binding ocorre quando você declara uma variável já especificando um tipo ao invés de um System.Object (que é a base), fortemente tipando e já tendo conhecimento do objeto em tempo de desenvolvimento e, além disso, você terá a checagem de tipos e conversões sendo feitas em compile-time, ou seja, não precisará esperar a aplicação ser executada para detectar possíveis problemas. Isso também irá permitir que o compilador trabalhe de forma mais eficiente, fazendo otimizações antes da aplicação efetivamente ser executada. Já o Late Binding é o processo inverso, ou seja, você somente irá conhecer o tipo em runtime. O Late Binding é menos performático que o Early Binding, mas te dará uma maior flexibilidade, flexibilidade qual é necessária quando trabalhamos com Reflection para criar e instanciar os membros de um determinado tipo. É justamente esse tipo de binding que vamos utilizar aqui. Antes de executar qualquer método ou propriedade, temos primeiramente que instanciar a classe em runtime e, para isso, existem duas formas. A primeira delas é utilizando o método de instância chamado CreateInstance da classe Assembly ou o método estático,

Page 334: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 20

Israel Aece | http://www.projetando.net

20

também chamado CreateInstance, da classe Activator. A diferença entre eles é que, no caso da classe Assembly, o tipo informado será procurado dentro do Assembly que a instância da classe Assembly está referenciado; já no caso da classe Activator, recebe o nome (identidade) de um Assembly de qual ela efetivamente deverá criar a instância da classe. Internamente, o método CreateInstance da classe Assembly, invoca o método CreateInstance da classe Activator. Ambos os métodos retornam um objeto do tipo System.Object com a instância da classe especifica criada. Ainda utilizando o exemplo da classe Cliente que construímos acima, vamos analisar como devemos procede para criar a instância da mesma em runtime. A classe Cliente está agora em um outro Assembly (uma DLL) em local físico diferente. Para fins de exemplo, a instância será criada a partir do método CreateInstance da instância da classe Assembly, qual foi criada com o retorno do método estático LoadFrom, também da classe Assembly, qual analisamos anteriormente. O código abaixo exemplifica como instanciar a classe Cliente: VB.NET Imports System.Reflection Dim asb As Assembly = Assembly.LoadFrom("C:\PeopleLibrary.dll") Dim cliente As Object = asb.CreateInstance( _ "PeopleLibrary.Cliente", _ False, _ BindingFlags.CreateInstance, _ Nothing, _ Nothing, _ Nothing, _ Nothing) C# using System.Reflection; Assembly asb = Assembly.LoadFrom(@"C:\PeopleLibrary.dll"); object cliente = asb.CreateInstance( "PeopleLibrary.Cliente", false, BindingFlags.CreateInstance, null, null, null, null); O método CreateInstance da classe Assembly é sobrecarregado e, em um dos seus overloads, é possível passar vários parâmetros para que o método cria a instância do objeto. Entre esses parâmetros, temos:

Page 335: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 21

Israel Aece | http://www.projetando.net

21

Parâmetro Descrição typeName Uma string representando o tipo que deverá ser instanciado. ignoreCase Um valor booleano indicando se a busca pelo tipo deverá ou não

ser case-sensitive. bindingAttr Uma combinação de valores disponibilizados no enumerador

BindingFlags que afetará na forma que a busca pelo tipo será efetuada. Entre os valores disponibilizados por esse enumerador, temos os principais deles descritos abaixo:

• CreateInstance – Especifica que o Reflection deverá criar uma instância do tipo especificado e chama o construtor que se enquandra com o array de System.Objects que pode ser passado para o método CreateInstance.

• DeclaredOnly – Somente membros declarados no mesmo nível da hierarquia do tipo será considerado na busca por membros e tipos.

• Default – Especifica o flag de no-binding. • ExactBinding – Esta opção especifica que os tipos dos

argumentos fornecidos devem exatamente coincidir com os tipos correspondentes dos parâmetros. Uma Exception é lançada se o chamador informar um Binder.

• FlattenHierarchy – Especifica que membros estáticos públicos e protegidos devem ser retornados. Membros estáticos privados não são retornados. Membros estáticos incluem campos, métodos, eventos e propriedades. Tipos aninhados não são retornados.

• GetField – Espefica que o valor de um campo especifíco deve ser retornado.

• GetProperty –Especifica que o valor de uma propriedade específica deve ser retornada.

• IgnoreCase – Especifica que o case-sensitive deve ser considerado quando efetuar o binding.

• IgnoreReturn – Usada em interoperabilidade com COM, ignora o valor de retorno do método.

• Instance – Especifica que membros de instância são incluídos na pesquisa do membro.

• InvokeMethod – Especifica que o método será invocado. • NonPublic – Especifica que membros não-públicos serão

incluídos na pesquisa do membro. • OptionalParamBinding – Esta opção é utilizada quando o

método possui parâmetros default. • Public – Especifica que membros públicos serão incluídos

na pesquisa de membros e tipos. • SetField – Este valor especifica que o valor de um membro

Page 336: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 22

Israel Aece | http://www.projetando.net

22

específico deve ser definido. • SetProperty – Espeifica que o valor de uma propriedade

específica deve ser definido. • Static – Especifica que membros estáticos serão incluídos

na pesquisa do membro. binder Um objeto que habilita o binding, coerção de tipos do argumentos,

invocação dos membros e recuperar a instância de objetos MemberInfo via Reflection. Se nenhum binder for informado, um binder padrão é utilizado.

args Um array de System.Object contendo os argumentos que devem ser passados para o construtor. Se o construtor for sobrecarregado, a escolha do qual utilizar será feito a partir do número de tipo dos parâmetros informados quando invocar o objeto.

culture Uma instância da classe CultureInfo que é utilizada para a coerção de tipos. Se uma referência nula for passado para este parâmetro, a cultura da thread corrente será utilizada.

activationAttributes Um array de elementos do tipo System.Object contendo um ou mais atributos que podem ser utilizados durante a ativação do objeto.

Se adicionarmos um construtor na classe Cliente, então devemos passar ao parâmetro args um array com os valores para satisfazer o overload, onde é necessário estar com o mesmo número de parâmetros, ordem e tipos. Para fins de exemplo, vamos, através do código abaixo, invocar a classe Cliente passando os objetos para o construtor que aceita um inteiro e uma string. O código muda ligeiramente: VB.NET Imports System.Reflection Dim asb As Assembly = Assembly.LoadFrom("C:\PeopleLibrary.dll") Dim constrArgs() As Object = {123, "José Torres"} Dim cliente As Object = asb.CreateInstance( _ "PeopleLibrary.Cliente", _ False, _ BindingFlags.CreateInstance, _ Nothing, _ constrArgs, _ Nothing, _ Nothing) C# using System.Reflection; Assembly asb = Assembly.LoadFrom(@"C:\PeopleLibrary.dll"); object[] constrArgs = { 123, "José Torres" }; object cliente = asb.CreateInstance(

Page 337: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 23

Israel Aece | http://www.projetando.net

23

"PeopleLibrary.Cliente", false, BindingFlags.CreateInstance, null, constrArgs, null, null); O método CreateInstance retorna um System.Object representando a instância da classe informada. Se caso o tipo não for encontrado, o método retorna uma valor nulo e, sendo assim, é necessário testar essa condição para não deixar a aplicação falhar. Em seguida, depois do objeto devidamente instanciado, vamos analisar como fazer para invocar os métodos e propriedades que o objeto possui. Para que possamos invocar os tipos do objeto, é necessário extrairmos o Type do mesmo como já vimos acima e, dando seqüência ao exemplo, as próximas linhas de código ilustram como extrair o objeto Type, através do método GetType da variável cliente: VB.NET Dim tipo As Type = cliente.GetType() C# Type tipo = cliente.GetType(); Essa classe tem um método denominado ExibeDados, que retorna uma string com o Id e o Nome do cliente concatenados. Como definimos no construtor da classe os valores 123 e “José Torres”, ao invocar o método ExibeDados, esses valores deverão ser exibidos. Para que seja possível invocar o método em runtime, necessitamos utilizar o método InvokeMember da classe Type, que retorna um System.Object com o valor retornado pelo método. O exemplo abaixo ilustra como utilizá-lo: VB.NET Console.WriteLine(tipo.InvokeMember( _ "ExibeDados", _ BindingFlags.InvokeMethod, _ Nothing, _ cliente, _ Nothing)) C# Console.WriteLine(tipo.InvokeMember( "ExibeDados", BindingFlags.InvokeMethod, null, cliente,

Page 338: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 24

Israel Aece | http://www.projetando.net

24

null)); Entre os parâmetros que esse método utiliza, temos (na ordem em que aparecem no código acima): uma string contendo o nome do construtor, método, propriedade ou campo a ser invocado (se informar uma string vazia, o membro padrão será invocado); uma combinação das opções fornecidas pelo enumerador BindingFlags (detalhado mais acima) indicando como a busca pelo membro será efetuada; binder a ser utilizado para a pesquisa do membro; a instância do objeto de onde o método será pesquisado e executado e, finalmente, um array de elementos do tipo System.Object, contendo os parâmetros necessários para ser passado para o método a ser executado. Além do método, ainda há a possibilidade de invocarmos propriedades em runtime. Podemos além de ler as informações de cada uma delas, podemos definir os valores a elas. Para isso, utilizamos o método GetProperty da classe Type, que retorna uma instância da classe PropertyInfo, que representa a propriedade e, através dela, definimos os valores e extraimos para escrevê-los, assim como é mostrado no trecho de código abaixo: VB.NET Dim nome As PropertyInfo = tipo.GetProperty("Nome") Dim id As PropertyInfo = tipo.GetProperty("Id") nome.SetValue(cliente, "Mario Oliveira", Nothing) id.SetValue(cliente, 456, Nothing) Console.WriteLine(nome.GetValue(cliente, Nothing)) Console.WriteLine(id.GetValue(cliente, Nothing)) C# PropertyInfo nome = tipo.GetProperty("Nome"); PropertyInfo id = tipo.GetProperty("Id"); nome.SetValue(cliente, "Mario Oliveira", null); id.SetValue(cliente, 456, null); Console.WriteLine(nome.GetValue(cliente, null)); Console.WriteLine(id.GetValue(cliente, null)); Basicamente, quando chamamos o método SetValue, passamos a instância do objeto onde as propriedades serão manipuladas; o novo valor a ser definido para a propriedade e, finalmente, um objeto do tipo System.Object, quando a propriedade se tratar de uma propriedade indexada. Já o método GetValue é quase idêntico, apenas não temos o valor a ser definido, pois como o próprio nome do método diz, ele é utilizado para ler o conteúdo da propriedade.

Page 339: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 25

Israel Aece | http://www.projetando.net

25

Finalmente, se quisermos extrair os eventos que a classe possui e vincularmos dinamicamente para que o mesmo seja disparado, podemos fazer isso através do método AddEventHandler fornecido pelo classe EventInfo. Como sabemos, a classe Cliente fornece um evento chamado AlterouDados, qual podemos utilizar para que quando um valor for alterado em uma das propriedades, esse evento seja disparado. O código abaixo ilustra como configurar para vincular dinamicamente o evento: VB.NET Dim ev As EventInfo = tipo.GetEvent("AlterouDados") ev.AddEventHandler(cliente, New EventHandler(AddressOf Teste)) ‘... Private Sub Teste(ByVal sender As Object, ByVal e As EventArgs) Console.WriteLine("Alterou...") End Sub C# EventInfo ev = tipo.GetEvent("AlterouDados"); ev.AddEventHandler(cliente, new EventHandler(Teste)); //... private void Teste(object sender, EventArgs e) { Console.WriteLine("Alterou..."); } Criação dinâmica de Assemblies Há um namespace dentro de System.Reflection chamado de System.Reflection.Emit. Dentro deste namespace existem várias classes que são utilizadas para criarmos dinamicamente um Assembly e seus respectivos tipos. Essas classes são também conhecidas como builder classes, ou seja, para cada um dos membros que vimos anteriormente, como por exemplo, Assembly, Type, Constructor, Event, Property, etc., existem uma classe correspodente com um sufixo em seu nome, chamado XXXBuilder, indicando que é um construtor de um dos itens citados. Para a criação de um Assembly dinâmico, temos as seguintes classes: Classe Descrição ModuleBuilder Cria e representa um módulo. EnumBuilder Representa um enumerador. TypeBuilder Fornece um conjunto de rotinas que são utilizados para

Page 340: Livro de .NET - Israel Aece

Capítulo 13 - Reflection 26

Israel Aece | http://www.projetando.net

26

criar classes, podendo adicionar métodos e campos. ConstructorBuilder Define um construtor para uma classe. EventBuilder Define um evento para uma classe. FieldBuilder Define um campo. PropertyBuilder Define uma propriedade para uma determinada classe. MethodBuilder Define um método para uma classe. ParameterBuilder Define um parâmetro. GenericTypeParameterBuilder Define um parâmetro genérico para classes e métodos. LocalBuilder Cria uma variável dentro de um método ou construtor. ILGenerator Gera código MSIL (Microsoft Intermediate Language).