msdn25_capa.jpg

Clique aqui para ler todos os artigos desta edição

 

Controles em runtime com Reflection e CodeDOM

 

 

Uma das características freqüentemente negligenciadas do Microsoft .NET Framework é a habilidade para gerar, compilar e executar código personalizado em tempo de execução (runtime). Isso é feito, por exemplo, durante a serialização dos dados XML e no uso de expressões regulares, onde a função de avaliação de expressões é gerada em tempo de execução.

Este artigo descreve outra área na qual a geração de código em tempo de execução pode ser usada: na criação de controles de IU (interface de usuário). Gerar esses controles uma única vez e então reutilizá-los quando necessário é muito mais eficiente que gerá-los cada vez que um formulário ou página são requisitados.

Isso se aplica a qualquer aplicação que tem campos configuráveis pelo usuário (por exemplo, quando o usuário final pode selecionar itens de dados a serem exibidos na tela). Geralmente definem-se formulários personalizados usando XML. Esses são analisados gramaticalmente em tempo de execução para a construção dinâmica da IU quando a página for carregada. Porém, essa análise gramatical freqüentemente acontece cada vez que o formulário é exibido, ou, em um cenário de servidor, para cada usuário, criando uma sobrecarga desnecessária para a aplicação.

Neste artigo, forneceremos exemplos detalhados de como usar a geração de código de tempo de execução para construir, carregar e executar controles em runtime. Os exemplos que descreveremos podem ser igualmente aplicados ao .NET Framework 1.x e 2.0. Existem alguns acréscimos significativos no namespace  Reflection na versão 2.0 do framework, mas essas mudanças não invalidam nem prejudicam de forma alguma, as soluções aqui apresentadas.

Fundamentos da geração de código

Muitas aplicações disponibilizam uma interface de usuário, a qual pode ser personalizada - possivelmente adicionando campos extras ou alterando a ordem e posição dos campos existentes. Isso é freqüentemente realizado em um dos seguintes dois modos:

·        O usuário faz mudanças editando a interface do usuário dentro do Visual Studio;

·        A aplicação gera controles em runtime baseados em algum formulário de dados de configuração, geralmente armazenado em um arquivo XML;

Nenhuma dessas soluções é ideal. A solução realmente necessária é uma combinação desses dois métodos tal que ofereça o desempenho da solução manual e a flexibilidade da aplicação que gera os controles em runtime.

Há dois métodos suportados para construir controles em runtime: Reflection.Emit e System.CodeDom. O primeiro, que requer íntimo conhecimento da Linguagem Intermediária (IL), gera assemblies gerenciados usando instruções IL explicitamente especificadas. O segundo, usa um modelo orientado a objeto para gerar código-fonte que pode ser compilado então em IL e executado. Ambos os métodos serão abordados neste artigo.

De fato existe um outro método, uma variante da abordagem CodeDOM, que consiste em construir código-fonte manualmente, significando escrever nós mesmos os arquivos .cs ou .vb, em lugar de usar o modelo de objeto para fazê-lo, e então fazer a compilação em tempo de execução. Apesar disso ser possível, recomendamos usar um dos mecanismos previamente mencionados em lugar de uma solução feita à mão.

Classes geradoras que usam Reflection.Emit

O namespace Reflection.Emit permite gerar assemblies que são completamente temporários; podem ser gerados em memória e executados sem nunca serem persistidos em disco. No entanto, caso necessário, a opção para escrever em disco está disponível (discutiremos isto mais adiante no artigo).

Para gerar uma classe que use o Reflection.Emit, são exigidos os seguintes passos:

·        Definir um assembly dinâmico;

·        Criar um módulo dentro do assembly;

·        Definir um tipo no módulo derivando o tipo da classe básica e interface(s) apropriada(s): criar os métodos naquele tipo, obter um ILGenerator para cada método, gerar o IL apropriado e persistir o tipo;

·        Opcionalmente, salvar o assembly para uso futuro.

A construção de classes que usam Reflection.Emit representa uma abordagem complexa, e para uma determinada operação de código-fonte (tal como chamar um método) teremos que gerar tipicamente várias linhas de código de IL. Mais adiante, retornaremos a esse assunto.

Gerando classes usando o System.CodeDom

O namespace System.CodeDom disponibiliza um modo independente de linguagem para definir tipos. Constrói um modelo em memória do código e então usa um gerador de código para gerar o código-fonte para aquele modelo. Contanto que tenha um gerador de código apropriado, pode gerar o mesmo em qualquer linguagem (o .NET Framework Redistributable inclui geradores de código para o Visual Basic, C# e JScript; caso o Visual Studio for instalado, geradores para C++ e J# também estarão disponíveis).

Para gerar uma classe usando o CodeDOM precisamos:

·        Criar uma nova CodeCompileUnit;

·        Acrescentar namespaces à CodeCompileUnit;

·        Adicionar as declarações de importação apropriadas;

·        Adicionar um CodeTypeDeclaration para a classe a ser construída;

·        Adicionar um CodeExpressions para cada método;

·        Obter um compilador de código e compilar o CodeCompileUnit.

Um dos benefícios desse método é o uso de conceitos de alto-nível, se comparado ao Reflection.Emit. Uma vez que as classes estejam definidas, precisamos construir instâncias em runtime. O modo mais comum é usar a classe Activator para carregar o tipo, como mostrado no seguinte trecho de código:

 

public Control LoadControl ( string typeName )

{

    return LoadControl(Type.GetType(typeName));

}

 

public Control LoadControl ( Type controlType )

{

    return Activator.CreateInstance(controlType) as Control;

}

 

Agora chamamos o membro GetType estático da classe Type. O GetType devolve a informação de tipo .NET para o tipo designado no argumento. A classe Activator é usada então para construir uma instância do tipo. Note que se o tipo especificado estiver em um assembly diferente de mscorlib ou do assembly atualmente em execução, o nome do tipo deve ser qualificado com o nome do assembly. Quando armazenados como uma string, os nomes de tipos gerenciados podem ser qualificados parcialmente com o namespace do tipo, ou totalmente com ambos o namespace do tipo e com o nome do assembly no qual está armazenado. Eis aqui uma instância de um nome de tipo qualificado no assembly:

 

TestControls.TestControl, TestAssembly

 

Esse nome de tipo corresponde a um tipo como mostrado no seguinte código, compilado em um assembly chamado TestAssembly:

 

using System.Web.IU;

 

namespace TestControls

{

  public class TestControl : Control

  {

    ...

  }

}

 

Ao gerar controles, normalmente armazenaríamos o nome de tipo em um meio de armazenamento persistente, como um servidor de banco de dados. Quando o formulário ou a página são apresentados, carregaríamos o tipo usando o Activator.CreateInstance e exibiríamos o objeto para o usuário.

Armazenando assemblies gerados

Assemblies criados usando o Reflection.Emit ou o System.CodeDom podem ser completamente temporários ou gerados em disco para uso futuro. Assemblies temporários existem apenas durante o ciclo de vida do domínio da aplicação (AppDomain) no qual são criados. Uma vez que este domínio da aplicação é descarregado (no caso do domínio de aplicação padrão, acontece quando a aplicação for descarregada), esses assemblies são descarregados da memória e deixam de existir. Desde que existe um custo para gerar esses assemblies em tempo de execução, recomendamos considerar a persistência dos assemblies gerados no disco. Se um assembly não pode ser achado no sistema de arquivos em tempo de execução, pode ser então gerado novamente. É claro que, se escolhermos essa abordagem, precisaremos garantir que a definição dos controles e os assemblies gerados permaneçam em sincronia. Em uma aplicação comercial, sugerimos proporcionar aos usuários uma ferramenta administrativa que possa percorrer todos os controles a serem gerados e os armazene em um único assembly que será persistido em disco ou dentro do servidor SQL.

Recomendamos também, limitar o número de assemblies gerados dinamicamente para o menor número prático – de preferência apenas um. Isso significa que o conjunto completo de controles precisará ser gerado cada vez que uma mudança seja feita em qualquer um. Porém, como essa é uma atividade que provavelmente não acontece muito freqüentemente, isso não deverá resultar em problemas de desempenho.

Segurança de assemblies gerados

Uma outra área importante a ser compreendida, refere-se aos aspectos de segurança associados com assemblies gerados. Isso é de enorme importância, pois os assemblies gerados representam uma ameaça ímpar para a aplicação - isto é, a aplicação em questão espera carregar alguns tipos dinamicamente, e estes tipos, teoricamente, poderiam executar qualquer código arbitrário.

A melhor forma para garantir a segurança do código, consiste em definir um conjunto limitado de permissões a todo código gerado. Isso pode ser realizado dentro do .NET, especificando-se um grupo de código com a ferramenta de configuração .NET mscorcfg.msc.

Um grupo de código customizado pode ser definido com uma condição de membro e um conjunto de permissões específicas. Como exemplo, poderíamos designar para todo código dinamicamente gerado um conjunto muito limitado de permissões, tal como Execute (o qual permite ao assembly executar, mas restringe seriamente o que o código pode fazer). Um grupo de código é aplicado a um assembly por meio de uma condição de membro – com efeito isso é usado para definir inclusões dentro do grupo. ...

Quer ler esse conteúdo completo? Tenha acesso completo