Desenvolvimento - ASP. NET

Páginas Assíncronas do ASP.NET 2.0 - Parte 1

Leia neste artigo a nova forma em que as páginas ASP.NET 2.0 trabalham e como as mesmas são processadas.

por Israel Aéce



function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i No momento em que o ASP.NET 1.x recebe uma requisição para uma determinada página, o runtime do ASP.NET resgata uma Thread do Pool e atribui esta à requisição para que a mesma seja executada. Isso faz com que todo o processo, desde a inicialização até a renderização, seja processado por essa Thread. Isso é conhecido como um processo (ou página) síncrono, ou seja, a Thread que está sendo utilizada somente será liberada no momento em que esse processo finalizar.

Neste caso, por processo eu me refiro à uma consulta a uma base de dados ou até mesmo uma chamada a um Web Service. Se a Thread está bloqueada/ocupada processando uma operação, quando novas requisições chegarem elas vão sendo enfileiradas, onde aguardam a liberação da Thread para que em seguida elas sejam processadas. Isso é problemático, pois se a fila "encher", será retornado ao usuário o erro do tipo 503 ("Server Unavailable"). A imagem abaixo ilustra o processo de requisição em uma aplicação ASP.NET 1.x:

Figura 1 - Requisição de uma página no ASP.NET 1.x.

Como vemos, depois do processo finalizado, a Thread é devolvida para o Pool. O ASP.NET 1.x não fornece um suporte para páginas assíncronas, mas isso não é impossível. O segredo está em implementar a interface IHttpAsyncHandler, mas isso já foge do escopo deste artigo. Ao contrário do ASP.NET 1.x, o ASP.NET 2.0 já traz instrinsicamente uma infra-estrutura para trabalharmos com páginas/chamadas assíncronas, simplificando bastante a sua utilização. A imagem abaixo ilustra como uma página ASP.NET 2.0 assíncrona trabalha:

Figura 2 - Requisição de uma página no ASP.NET 2.0.

Os detalhes para a utilização das páginas assíncronas começam efetuando uma configuração na página ASPX, ou seja, devemos definir na diretiva de página um novo atributo chamado Async, que recebe um valor booleano (Verdadeiro ou Falso) indicando se a página irá ter ou não um processamento assíncrono. O trecho de código abaixo exibe essa configuração no arquivo ASPX:

<%@ Page Async="True" ... %>
ASPX

Depois desta configuração, que é extremamente necessária para esse tipo de processamento, devemos chamar o método AddOnPreRenderCompleteAsync da classe Page no evento Load do WebForm. Esta função receberá dois handlers/delegates: o procedimento que o ASP.NET executará assincronamente (BeginEventHandler) e o procedimento que será executado quando o mesmo for finalizado (EndEventHandler). Como já dissemos anteriormente, podemos fazer aqui uma chamada à um Web Service ou efetuar uma query a uma base de dados e, conseqüentemente, exibir os dados para o usuário. Veremos abaixo como configurar o método AddOnPreRenderCompleteAsync no evento Load do WebForm, onde vamos passar os procedimentos que deverão ser executados:

Page.AddOnPreRenderCompleteAsync(
    new BeginEventHandler(IniciaProcesso),
    new EndEventHandler(FinalizaProcesso)
);
Page.AddOnPreRenderCompleteAsync( _
    New BeginEventHandler(AddressOf IniciaProcesso), _
    New EndEventHandler(AddressOf FinalizaProcesso) _
)
C# VB.NET

Quando a requisição à esta página acontece, o ASP.NET resgata a Thread do Pool e executa os passos (eventos) até o evento PreRender do WebForm e devolve esta Thread ao Pool. O procedimento que é disparado assincronamente deve retornar um IAsyncResult, que irá determinar se a operação foi completada. Neste momento, uma nova Thread é recuperada do Pool e executa o segundo procedimento (o qual também passamos para o método AddOnPreRenderCompleteAsync ("FinalizaProcesso")). Depois que esse procedimento finalizar, essa mesma Thread executa o restante dos eventos que compõem o ciclo de vida de uma página ASP.NET até o seu retorno ao cliente, ou seja, quando o Response é criado. Isso explica detalhadamente o que vimos na Figura 2. Mas a desvantagem neste caso é que dentro do procedimento "FinalizaProcesso" não temos acesso ao contexto da requisição (HttpContext.Current), porém temos formas de conseguir contornar isso, que veremos em um futuro artigo.

Este tipo de implementação não prende as requisições subseqüentes até que o processo seja finalizado, ou seja, quando a parte mais complexa/custosa de uma página começar a ser executada, já devolvemos a Thread ao Pool para que a mesma possa servir as próximas requisições enquanto o processo assíncrono acontece. A Figura 3 nos ajuda a comparar os eventos de uma página síncrona versus uma página assíncrona para ver e perceber o momento exato em que as coisas acontecem, podendo ser confirmado através do Trace de uma determinada página assíncrona, que é mostrado na Figura 4, logo em seguida.

Figura 3 - Ciclo de Vida - Síncrona vs. Assíncrona.

Figura 4 - Trace de uma página Assíncrona.

Acesso à Base de Dados

É bastante comum em páginas ASP.NET acessarmos o conteúdo de uma determinada base de dados; sendo assim, podemos efetuar uma query assíncrona dentro de uma base de dados qualquer e retornar ao usuário o result-set e, conseqüentemente, popular um controle do tipo GridView. Devemos, neste caso, criar e codificar o evento PreRenderComplete, que será disparado imediatamente depois do processo assíncrono finalizado, mas antes da página ser renderizada, justamente para termos acesso ao controle GridView e aos demais itens da página.

A execução assíncrona de queries na base de dados é possível devido às novas funcionalidades que temos dentro do ADO.NET 2.0, ou seja, o objeto SqlCommand ganhou vários novos métodos para executar trabalho assíncrono. Entre esses novos métodos temos: BeginExecuteReader e EndExecuteReader. Além disso, a ConnectionString também deverá acrescer um novo parâmetro ([...];Asynchronous Processing=true) para indicar que a mesma poderá trabalhar de forma assíncrona. Isso irá fazer com que a nossa aplicação se torne muito mais escalável, pois como já vimos anteriormente, o processo não prenderá a Thread até que a query seja retornada. Veremos abaixo o código necessário para a execução de uma query assíncrona dentro de uma página também assíncrona:

using System.Data.SqlClient;

public partial class DB : System.Web.UI.Page
{
    private const string CONN_STRING = @"[...];Asynchronous Processing=true";
    private SqlConnection _conn;
    private SqlCommand _cmd;
    private SqlDataReader _reader;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            this.PreRenderComplete += new EventHandler(Page_PreRenderComplete);

            this.AddOnPreRenderCompleteAsync(
                new BeginEventHandler(IniciaProcesso),
                new EndEventHandler(FinalizaProcesso)
            );
        }
    }

    protected IAsyncResult IniciaProcesso(
        object sender, 
        EventArgs e, 
        AsyncCallback cb, 
        object state)
    {
        this._conn = new SqlConnection(CONN_STRING);
        this._conn.Open();
        this._cmd = new SqlCommand("SELECT * FROM Usuarios", this._conn);
        return this._cmd.BeginExecuteReader(cb, state);
    }

    protected void FinalizaProcesso(IAsyncResult ar)
    {
        this._reader = this._cmd.EndExecuteReader(ar);
    }

    protected void Page_PreRenderComplete(object sender, EventArgs e)
    {
        if (this._reader.HasRows){
            this.GridView1.DataSource = _reader;
            this.GridView1.DataBind();		
        }
    }

    public override void Dispose()
    {
        if (this._conn != null) this._conn.Close();
        base.Dispose();
    }
}
Imports System.Data.SqlClient

Partial Class DB

    Inherits System.Web.UI.Page

    Private Const CONN_STRING As String = "[...];Asynchronous Processing=true"
    Private _conn As SqlConnection
    Private _cmd As SqlCommand
    Private _reader As SqlDataReader

    Protected Sub Page_Load( _
        ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load

        If Not Page.IsPostBack Then
            Me.AddOnPreRenderCompleteAsync( _
                New BeginEventHandler(AddressOf IniciaProcesso), _
                New EndEventHandler(AddressOf FinalizaProcesso) _
            )
        End If
    End Sub

    Protected Function IniciaProcesso( _
        ByVal sender As Object, _
        ByVal e As EventArgs, _
        ByVal cb As AsyncCallback, _
        ByVal state As Object) As IAsyncResult

        Me._conn = New SqlConnection(CONN_STRING)
        Me._conn.Open()
        Me._cmd = New SqlCommand("SELECT * FROM Usuarios", Me._conn)
        Return Me._cmd.BeginExecuteReader(cb, state)
    End Function

    Protected Sub FinalizaProcesso(ByVal ar As IAsyncResult)
        Me._reader = Me._cmd.EndExecuteReader(ar)
    End Sub

    Protected Sub Page_PreRenderComplete( _
        ByVal sender As Object, _
        ByVal e As EventArgs) Handles Me.PreRenderComplete

        If Me._reader.HasRows Then
            Me.GridView1.DataSource = Me._reader
            Me.GridView1.DataBind()
        End If
    End Sub

    Public Overrides Sub Dispose()
        If Not IsNothing(Me._conn) Then Me._conn.Close()
        MyBase.Dispose()
    End Sub

End Class
C# VB.NET

Analisando o código acima, vemos que no evento Load do WebForm definimos o método que será executado assincronamente e também o método que será executado quando o processo for finalizado. Quando o processo se inicia, o código do procedimento "IniciaProcesso" é executado, ou seja, fazemos a consulta na base de dados e, quando finalizado, o procedimento "FinalizaProcesso" é executado e nele atribuimos o retorno do método EndExecuteReader, que irá retornar um objeto do tipo SqlDataReader para o membro _reader da página.

Se analisarmos novamente a Figura 4, vemos que o evento PreRenderComplete é disparado depois que o procedimento assíncrono é finalizado, e é neste momento que devemos atribuir o resultado ao nosso controle GridView, pois aqui a página não foi ainda renderizada. Finalmente sobrescrevemos o método Dispose da classe base (Page), onde fazemos o fechamento da conexão com a base de dados. Não esquecer em hipótese alguma de chamar também o método Dispose da classe base.

Vale lembrar também que o método BeginExecuteReader tem um overload (sobrecarga) que pode receber como parâmetro um enumerador do tipo CommandBehavior, onde podemos definí-lo como CloseConnection e, quando fecharmos a conexão com o DataReader através do seu método Close, a conexão com a base de dados é automaticamente fechada. Quando definimos o CommandBehavior como CommandBehavior, o DataReader já será fechado automaticamente depois do método DataBind ser executado e, conseqüentemente, fechando a conexão com a base de dados que está vinculada a ele. Faz sentido ele fechar logo após o método DataBind pois, como sabemos, o DataReader somente avança, não faz mais sentido termos ele "ativo", já que o GridView o utilizou.

Acesso à Web Services

Os WebServices já fornecem por padrão métodos para serem executados assincronamente, como já deve ser conhecido de todos. Para certificar-se disso, experimente criar um método qualquer em seu arquivo ASMX marcado com o atributo WebMetod e verá que, quando fizer uma Web Reference em seu projeto verá que, além do método criado, também serão disponibilizados mais dois métodos, como por exemplo: se definiu o método chamado "RecuperaUsuarios" então terá também "BeginRecuperaUsuarios" e "EndRecuperaUsuarios". Veremos abaixo como invocar esses WebServices assincronamente:

using System.Data;

public partial class WS : System.Web.UI.Page
{
    private DataSet _ds;
    private UsuariosWS.Usuarios _wsUsuarios;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            this.PreRenderComplete += new EventHandler(Page_PreRenderComplete);

            this.AddOnPreRenderCompleteAsync(
                new BeginEventHandler(IniciaProcesso),
                new EndEventHandler(FinalizaProcesso)
            );
        }
    }

    protected IAsyncResult IniciaProcesso(
        object sender,
        EventArgs e,
        AsyncCallback cb,
        object state)
    {
        this._wsUsuarios = new UsuariosWS.Usuarios();
        this._wsUsuarios.UseDefaultCredentials = true;
        return this._wsUsuarios.BeginRecuperaUsuarios(cb, state);
    }

    protected void FinalizaProcesso(IAsyncResult ar)
    {
        this._ds = this._wsUsuarios.EndRecuperaUsuarios(ar);
    }

    protected void Page_PreRenderComplete(object sender, EventArgs e)
    {
        this.GridView1.DataSource = this._ds.Tables[0];
        this.GridView1.DataBind();
    }

    public override void Dispose()
    {
        if (this._wsUsuarios != null) this._wsUsuarios.Dispose();
        base.Dispose();
    }
}
Imports System.Data

Partial Class WS

    Inherits System.Web.UI.Page

    Private _ds As DataSet
    Private _wsUsuarios As UsuariosWS.Usuarios

    Protected Sub Page_Load( _
        ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load

        If Not Page.IsPostBack Then
            Me.AddOnPreRenderCompleteAsync( _
                New BeginEventHandler(AddressOf IniciaProcesso), _
                New EndEventHandler(AddressOf FinalizaProcesso) _
            )
        End If
    End Sub

    Protected Function IniciaProcesso( _
        ByVal sender As Object, _
        ByVal e As EventArgs, _
        ByVal cb As AsyncCallback, _
        ByVal state As Object) As IAsyncResult

        Me._wsUsuarios = New UsuariosWS.Usuarios()
        Me._wsUsuarios.UseDefaultCredentials = True
        Return Me._wsUsuarios.BeginRecuperaUsuarios(cb, state)
    End Function

    Protected Sub FinalizaProcesso(ByVal ar As IAsyncResult)
        Me._ds = Me._wsUsuarios.EndRecuperaUsuarios(ar)
    End Sub

    Protected Sub Page_PreRenderComplete( _
        ByVal sender As Object, _
        ByVal e As EventArgs) Handles Me.PreRenderComplete

        Me.GridView1.DataSource = Me._ds.Tables(0)
        Me.GridView1.DataBind()
    End Sub

    Public Overrides Sub Dispose()
        If Not IsNothing(Me._wsUsuarios) Then Me._wsUsuarios.Dispose()
        MyBase.Dispose()
    End Sub

End Class
C# VB.NET

Se analisarmos o código acima, veremos que o mecanismo é bem parecido ao que utilizamos para efetuar o acesso/query à base de dados utilizando o método AddOnPreRenderCompleteAsync da classe Page para definir o processo assíncrono. A única diferença aqui é que o retorno, que recuperamos através do método EndRecuperaUsuarios, é atribuído ao DataSet, que é um membro da página. Este por sua vez, é definido como fonte de dados para o controle GridView.

Além dessas novas funcionalidades, temos ainda algumas outras que não abordei aqui, como por exemplo o método [Metodo]Async e o evento [Metodo]Completed, os quais são encontrados nos proxies dos Web Services do .NET Framework 2.0. Temos também as chamadas Asynchronous Tasks, ou seja, tarefas assíncronas. Ambos fornecem algumas vantagens em relação ao que vimos no decorrer deste artigo, onde deixarei para explicá-las em um futuro e próximo artigo.

CONCLUSÃO: Vimos neste artigo a nova forma em que as páginas ASP.NET 2.0 trabalham e como as mesmas são processadas. Conseguimos através das figuras, que comparam a versão 1.x versus a versão 2.0, perceber o ganho e a flexibilidade que agora são fornecidos internamente pelo ASP.NET 2.0, sem a necessidade de escrevermos muito código para alcançar este objetivo.
Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.