Injeção de Dependência em ASP.NET Webforms

Muito do material que vemos pela Internet a respeito de Inversão de Controle e Injeção de Dependência está relacionado ao ASP.NET MVC. Hoje o meu objetivo aqui é mostrar como fazer uso de um contêiner de IoC com o "patinho feio" do ASP.NET, o Webforms.

Mas antes, vamos contextualizar o que é Inversão de Controle e Injeção de Dependências. Inversão de Controle é um conceito de programação onde é tirada a responsabilidade de uma classe em criar as suas dependências. Vejamos o código abaixo. Nele, a página ASP.NET (que é uma classe) instancia uma classe chamada SaudacaoPTBr e chama seu método Saudar para retornar uma informação qualquer. Por isso, esta página depende de SaudacaoPTBr. Perceba que para utilizar essa dependência, a página ASP.NET teve que, no seu próprio código, criar um objeto do tipo SaudacaoPTBr.

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var saudacao = new SaudacaoPTBr();
        Label1.Text = saudacao.Saudar();
    }
}

Na Inversão de Dependência, a página ASP.NET não poderia mais fazer isso. Lhe é tirada essa responsabilidade. E nesse conceito, como seria o código que temos que usar? A Inversão de Dependência nos dá algumas opções para implementá-la:
  • Podemos usar o padrão Factory
  • Podemos usar o padrão Service Locator
  • Podemos usar Injeção de Dependência
Vamos nos focar neste último.

A primeira coisa que vamos fazer é tirar a dependência da página ASP.NET com a classe SaudacaoPTBr. Fazemos isso criando uma interface (ISaudacao) e criando uma propriedade com essa interface na nossa página ASP.NET.

public interface ISaudacao
{
    string Saudar();
}

public partial class _Default : System.Web.UI.Page
{
    [Inject]
    public ISaudacao Saudacao { get; set; }

    protected void Page_Load(object sender, EventArgs e)
    {
        Label1.Text = Saudacao.Saudar();
    }
}

A ideia aqui é fazer com que "alguém" inicialize a propriedade Saudacao com a correta instância para que ela possa ser usada no código e faça seu trabalho. Ou seja, precisaríamos de alguém que fizesse algo parecido com o código abaixo: criar uma instância da página web, preencher sua propriedade (sua dependência) com o objeto correto, e retornar essa página pronta para uso.

private _Default CriarPaginaDefault()
{
    var pagina = new _Default();
    pagina.Saudacao = new SaudacaoPTBr();
    return pagina;
}

Veja aqui o conceito de Inversão de Controle em ação. A página Default.aspx não tem ideia de quem será a classe concreta que ela vai utilizar para obter uma saudação, apenas conhece o contrato que ela deve seguir.

Bom, o código de exemplo acima não funciona. Não somos nós que criamos as instâncias das páginas web, é o próprio runtime do ASP.NET que faz isso. Felizmente o ASP.NET permite definirmos novas classes de factory (fábrica) para criar as instâncias das páginas. Assim, o runtime utilizará a classe factory que nós definirmos. Isso é feito com dois passos:
  1. Criar uma classe factory que siga o contrato da interface IHttpHandlerFactory.
  2. Registrar o uso dessa interface no web.config.
Abaixo está nossa implementação da interface IHttpHandlerFactory. Note que ao invés de implementar cada um dos três métodos que ela possui, estou fazendo herança da classe PageHandlerFactory. Bom, eu não estou afim de ter que codificar tudo que é necessário para instanciar uma página ASP.NET, então vou apenas estender o método já existente que faz isso, apenas adicionando a dependência da página.

public class DIPageFactory : PageHandlerFactory
{
    public override IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string path)
    {
        var pagina = (Page)base.GetHandler(context, requestType, virtualPath, path);
        if (virtualPath == "/default.aspx")
            (pagina as _Default).Saudacao = new SaudacaoPTBr();
        return pagina;
    }
}

Agora vai como fica no web.config.
<httphandlers>
      <add path="*.aspx" type="ExemploDIWebForms.Codigo.DIPageFactory, ExemploDIWebForms" verb="*">
    </add>
</httphandlers>

Pronto, assim toda vez que qualquer página ASP.NET for criada, essa criação passará por este método. E desta maneira, podemos incluir a criação das dependências necessárias que a página possui. É isso, gostaram? Não? Acharam feio o if que coloquei para adicionar a dependência quando a página for a Default.aspx? É..., realmente não é legal deixar as coisas cravadas assim. Vamos melhorar o código, e para isso precisamos de alguém que faça a atribuição das dependências de forma correta, para cada página que formos usar.

Quem fará esse trabalho de ligar a interface (contrato) com a classe concreta (implementação) será um contêiner de Injeção de Dependência. E para esse trabalho vamos escolher, dentre vários frameworks disponíveis, o Ninject. Mas poderia ser qualquer outro (Spring.NET, Unity, Castle Windsor), o que importa aqui é apresentar o conceito.

A ideia é a seguinte: vamos precisar que, quando uma classe tiver uma dependência que seja ISaudacao, ela seja automaticamente preenchida com um objeto SaudacaoPTBr. Para isso, precisamos criar uma forma de mapear o contrato (interface) para a classe concreta (implementação). Vamos usar um módulo do Ninject (uma classe que herda de NinjectModule). Veja que estamos fazendo uma sobrecarga do método Load, especificando que quando encontrarmos uma interface ISaudacao, ela deve ser preenchida com SaudacaoPTBr.

public class ModuloInjecoes : NinjectModule
{
    public override void Load()
    {
        Bind<ISaudacao>().To<SaudacaoPTBr>();
    }
}

Agora podemos deixar genérico a nossa classe factory! Basta usar o contêiner do Ninject para que ele resolva os contratos para nós.

public class DIPageFactory : PageHandlerFactory
{
    public override IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string path)
    {
        var handler = (Page)base.GetHandler(context, requestType, virtualPath, path);
        var kernel = new StandardKernel(new ModuloInjecoes());
        kernel.Inject(handler);
        handler.InitComplete += (sender, e) =>
        {
            foreach (Control controle in handler.Controls)
                InicializarUserControls(kernel, controle);
        };
        return handler;
    }

    private void InicializarUserControls(IKernel kernel, Control controle)
    {
        if (controle is UserControl)
            kernel.Inject(controle);
        foreach (Control filhos in controle.Controls)
            InicializarUserControls(kernel, filhos);
    }
}

O contêiner é a classe StandardKernel, do Ninject. Quando nós a instanciamos, passamos para ela uma instância de ModuloInjecoes, aquela classe que criamos e que explica para o framework de IoC qual classe deve-se criar para cada interface. Depois disso, chamamos o método Inject do contêiner. Ele irá então procurar por todas as dependências que foram decoradas com o atributo Inject (se alguém estava se perguntando porque tinha esse atributo no segundo trecho de código, aí está a razão), e inicializá-las corretamente.

Um detalhe importante. O Ninject sozinho não inicializa as dependências dos controles filhos da página ASP.NET. Isso quer dizer que se tivermos um User Control dentro da nossa página e que também possua uma dependência, ele não será inicializado. Resolvemos isso criando um método que percorre todos os controles filhos da página, recursivamente, para que quando se encontrar um User Control, que seja chamado o método Inject do Ninject para que suas dependências sejam preenchidas. No caso de uma Master Page, ela também é considerada como um User Control, então nosso método InicializarUserControls irá funcionar para elas também. E esse método é então chamado no evento InitComplete da página (só tomem cuidado que deve-se colocar, dentro do User Control, código que execute apenas a partir do seu evento Load, se for colocado no Init as dependências ainda não terão sido inicializadas, então você tomará um erro de "Object not set to an instance of an object").

Indo além


Eu não fui a primeira pessoa a precisar usar injeção de dependência. Procurando um pouco mais pela internet, encontrei uma extensão do Ninject exatamente para trabalhar com WebForms: https://github.com/ninject/ninject.web.

Ela segue o mesmo conceito de interceptar os eventos que ocorrem no pipe line de construção das páginas pelo ASP.NET. Entretanto, eles usam módulos http que são configurados ao invés do factory de páginas (veja a classe NinjectHttpModule). Outra diferença é que essa extensão se baseia em classes base para os user controls, páginas e master pages. De qualquer maneira, é uma implementação já pronta e é interessante considerá-la ao invés de escrever um novo (ou seja, estou falando que o meu é "reinventar a roda", e na verdade é mesmo!).

Outra consideração que eu gostaria de fazer ao meu exemplo é sobre a forma que deixei para que as dependências sejam injetadas. Usei propriedades. Existem críticas em relação a essa abordagem, pois com propriedades o desenvolvedor não sabe quais dependências ele deve injetar. Isso difere de uma injeção via construtor, onde você obrigatoriamente tem que passar as dependências senão não será possível criar o objeto. Outra justificativa de ter usado injeção de dependência por propriedade é que nós não temos controle do código que cria os user controls, apenas temos acesso a eles depois de criados.

Abaixo alguns links de discussões a respeito:
[]'s

Comentários

  1. Onde posso baixar o projeto utilizado como exemplo?
    Estou com dificuldades de entender onde os trechos de código acima foram criados e chamados

    ResponderExcluir
    Respostas
    1. Olá! Infelizmente não encontrei o código de exemplo, mas me diga a sua dúvida, talvez eu possa ajudar.

      Excluir
  2. Onde coloco cada trecho de código?

    ResponderExcluir
    Respostas
    1. Você pode colocar cada classe / interface (ISaudacao, ModuloInjecoes, DIPageFactory) como um arquivo separado na pasta App_Code do seu projeto ASP.NET. A configuração de httphandlers você coloca no seu arquivo web.config, dentro da tag system.web. _Default você cria como uma página web.
      Obs. Percebi que esqueci de colocar o arquivo SaudacaoPTBr, mas ela é uma classe que implementa a interface ISaudacao. Como é um teste, você pode deixar o método Saudar dela retornando uma string constante qualquer.

      Excluir

Postar um comentário

Postagens mais visitadas deste blog

Trocando configurações padrão do Live TIM

Uma proposta de Clean Architecure com Modelo de Atores

Testes automatizados em sistemas autenticados com certificados digitais, usando Selenium e PhantomJS