WCF – Chamadas Assíncronas

Muitas vezes desenvolvemos um método para desempenhar alguma tarefa e, depois de devidamente codificado, invocamos o mesmo a partir de algum ponto da aplicação. Dependendo do que este método faz, ele pode levar certo tempo para executar e, se o tempo for consideravelmente alto, podemos começar a ter problemas na aplicação, pois como a chamada é sempre realizada de síncrona, enquanto o método não retornar, a execução do sistema que faz o uso do mesmo irá congelar, aguardando o retorno do método para dar seqüência na execução.

Entretanto, para fornecer uma melhor experiência ao usuário e tornar o desempenho da aplicação muito mais performático, podemos optar por executar, uma determinada tarefa de forma assíncrona. Isso irá possibilitar o disparo de um processo custoso em uma thread de trabalho a parte da aplicação, permitindo assim que a aplicação continue funcionando enquanto o processo custoso é executado em paralelo e, quando o mesmo finalizar, podemos recuperar o resultado e exibir o mesmo ao usuário.

A finalidade deste artigo é mostrar como implementar o processamento assíncrono tanto do lado do cliente (proxy) bem como do lado do servidor (contrato) em serviços WCF. Antes de prosseguir, é necessário que estejamos familiarizados com a arquitetura de processamento assíncrono dentro da plataforma .NET. Basicamente, temos alguns métodos dentro do próprio .NET Framework que existem 3 versões para o mesmo: o primeiro deles é para uso síncrono; já os outros dois (BeginXXX e EndXXX) são para a chamada assíncrona deste mesmo processo. Convencionou-se que métodos síncronos são nomeados de acordo com a sua funcionalidade, exemplo: Read, Write; já, as versões assíncronas destes mesmos métodos são: BeginRead, EndRead, BeginWrite e EndWrite.

A versão 2.0 do ADO.NET também já traz suporte ao processamento assíncrono para tarefas comuns de acesso a dados, como a execução de queries complexas para modificação de registros no banco de dados e também quando necessitamos recuperar dados com alguma query mais complexa (mais detalhes podem ser vistos neste artigo). O suporte ao processo assíncrono não para por aí, acesso aos arquivos no disco, acesso a serviços e até mesmo os delegates trazem nativamente esse suporte.

Processamento Assíncrono no Cliente

Neste cenário, nada é necessário ser realizado durante a criação e implementação do contrato. Isso funcionará de forma muito semelhante ao que já acontecia com os artigos ASP.NET Web Services, ou seja, para cada método criado no contrato, é também criado uma versão assíncrona (BeginXXX e EndXXX) do método quando fazemos a referência ao serviço no cliente. Aqui há ligeira mudança: os métodos que dão suporte assíncrono não são implicitamente criados. A imagem abaixo ilustra o local que devemos marcar para definir a criação dos métodos que dão suporte ao processo assíncrono:

Figura 1 – Ao efetuar uma Add Service Reference, clique na opção Advanced para marcar a opção que está marcada em vermelho.

Por padrão, esta opção não está marcada. Uma vez que você opta por marcá-la, será criada uma versão assíncrona para cada método que há dentro do contrato. A partir daí o desenvolvedor que consume o proxy do serviço precisará se atentar para efetuar a chamada para o método de forma assíncrona, o que exigirá a forma em que a chamada será executada. Quando se trata um processo assíncrono, temos alguns cuidados a respeito do disparo do método: o método que dá início ao processo (BeginXXX) geralmente espera os parâmetros necessários para a execução da tarefa (caso exista), uma instância de um delegate do tipo AsyncCallback (explorado mais abaixo) e, finalmente, um object que representa uma informação “contextual”.

Observação: A opção “Generate asynchronous operations” é o mesmo que especificar o parâmetro /async do utilitário svcutil.exe.

Para exemplificar, temos um contrato chamado ICliente que possui um método chamado CalcularComissao que, por sua vez, retornará um valor que representa o valor calculado das comissões. Essa tarefa trata-se de um processo que, para fins de exemplo, será encarado como uma tarefa custosa e que levará certo tempo para executar. O código abaixo ilustra o contrato a ser utilizado (note que não há nada de especial):

using System;
using System.ServiceModel;

[ServiceContract]
public interface ICliente
{
    [OperationContract]
    decimal CalcularComissao(string codigo);
}

Uma vez adicionado à referência, podemos notar que haverá três métodos a nossa disposição do lado do cliente: CalcularComissao para processo síncrono, BeginCalcularComissao e EndCalcularComissao para processamento assíncrono. Durante a geração do proxy o método BeginXXX é decorado com o atributo OperationContractAttribute que, por sua vez, tem uma propriedade chamada AsyncPattern e, neste caso, está definida como True. Com essa configuração, o runtime não invocará o método BeginCalcularComissao do contrato, mesmo porque ele não existe; na verdade o runtime irá utilizar uma thread do ThreadPool para disparar sincronamente o método definido pela propriedade Action e, que neste caso é o método CalcularComissao.

Vou supor que a chamada para o método de forma síncrona já é conhecida e está sedimentada. O foco aqui será entender o funcionamento do processo assíncrono. O ponto de partida é o método BeginCalcularComissao: ele dará início ao processo e, além de receber o callback e o object como parâmetro (como já foi dito acima), esse método também espera os parâmetros que o próprio método exige para desempenhar a tarefa a qual ele se destina a fazer. Seguindo a arquitetura de processamento assíncrono do .NET Framework, o método BeginCalcularComissoes retorna um objeto que implementa a Interface IAsyncResult; esse objeto armazena uma referência para o processo que está sendo executado em paralelo.

Como já pudemos notar, não é o método BeginXXX que retorna o resultado. Isso é uma tarefa que pertence exclusivamente ao método EndXXX. A questão é quando invocá-lo. Uma vez que você invoca o mesmo, e o processo ainda não tenha sido finalizado, o programa trava a execução, esperando pelo retorno do processo. Dependendo do quanto tempo ainda falta para finalizar, podemos cair no mesmo problema do processamento síncrono. O trecho de código abaixo ilustra como consumir o método BeginCalcularComissoes:

using (ClienteClient proxy = new ClienteClient())
{
    IAsyncResult ar = proxy.BeginCalcularComissao("123", null, null);

    //fazer algum trabalho

    decimal resultado = proxy.EndCalcularComissao(ar);
    Console.WriteLine(resultado);
}

No exemplo acima podemos ter algum benefício, pois o método BeginCalcularComissao dispara o processamento assíncrono e já devolve o controle da execução para o programa e, conseqüentemente, permite que façamos algum trabalho em paralelo enquanto o serviço calcula as comissões. O problema é que se no momento da chamada do método EndCalcularComissao (que retornará o resultado) o processo ainda não finalizou, a execução irá bloquear a execução até que o processo assíncrono retorne. Vale lembrar que essa técnica às vezes é necessária: imagine um momento em que o programa não pode continuar a sua execução, pois depende o resultado do processo assíncrono para continuar.

Há ainda uma outra possibilidade de testar o retorno do processo assíncrono, que é chamado de pooling. Essa técnica consiste em antes de invocar o método EndCalcularComissao e possivelmente bloquear a execução, podemos testar se o processo finalizou ou não. Se repararmos no exemplo de código acima, o método BeginCalcularComissao retorna um objeto que implementa a Interface IAsyncResult. Essa Interface fornece um método chamado IsCompleted que retorna um valor booleano indicando se o processo foi ou não finalizado. Isso garantirá que chamaremos o método EndCalcularComissao somente que o processo realmente finalizou. O código abaixo ilustra o uso desta propriedade:

using (ClienteClient proxy = new ClienteClient())
{
    IAsyncResult ar = proxy.BeginCalcularComissao("123", null, null);

    //fazer algum trabalho

    if (ar.IsCompleted)
    {
        decimal resultado = proxy.EndCalcularComissao(ar);
        Console.WriteLine(resultado);
    }
    else
    {
        Console.WriteLine("O processo ainda não finalizou.");
    }
}

Finalmente, a última técnica que temos para disparar um método de forma assíncrona é com a utilização de callbacks. Com esta alternativa, ao invés de ficarmos testando se o processo finalizou ou não, ao invocar o método BeginCalcularComissao, passamos uma instância da classe AsyncCallback com a referência para um método do lado do cliente que deve ser disparado com o processo assíncrono for finalizado.

Uma vez especificado o método que será utilizado como callback, não é mais necessário que você armazene o objeto que armazena a referência para o processo assíncrono (IAsyncResult), pois isso será automaticamente fornecido para o callback; além disso, ainda é importante dizer que o método de callback é executado sob a thread de trabalho, antes dela ser devolvida para o ThreadPool. Como já foi comentado acima, informamos o método a ser disparado a partir de uma instância do delegate AsyncCallback, como é mostrado no código abaixo:

private static ClienteClient _proxy;

private static void TestandoProcessoAssincronoComCallback()
{
    _proxy = new ClienteClient();
    _proxy.BeginCalcularComissao("123", new AsyncCallback(Callback), null);
    Console.ReadLine();
}

private static void Callback(IAsyncResult ar)
{
    decimal resultado = _proxy.EndCalcularComissao(ar);
    Console.WriteLine(resultado);
}

Observação: Podemos recorrer à utilização de métodos anônimos ou até mesmo das expressões lambda para evitar a criação de um método a parte para ser disparado quando o callback acontecer.

Além do tradicional modelo de chamadas assíncronas (APM), temos a possibilidade da chamada assíncrona baseada em eventos. A idéia aqui é, antes de invocar a operação, você poderá assinar à um evento que será disparado somente quando o processo assíncrono finalizar. Isso evitará de ter um trabalho manual para analisar se o processo finalizou ou não (poll, waiting, etc.). Internamente durante a geração do proxy, o código que é auto-gerado já inclui a implementação necessária para o modelo baseado em eventos.

Basicamente é criado mais uma versão do método, agora com o sufixo XXXAsync que, internamente, faz a chamada para os métodos BeginXXX/EndXXX que, como já sabemos, dispararam a operação de forma assíncrona. Além disso, um delegate do tipo EventHandler<T> também será criado para representar o callback que, quando disparado, invocará o evento do lado de quem está consumindo o serviço. Abaixo um exemplo de como efetuar a chamada assíncrona baseada em eventos:

using (ClienteClient proxy = new ClienteClient())
{
    proxy.CalcularComissaoCompleted += 
        new EventHandler<CalcularComissaoCompletedEventArgs>(proxy_CalcularComissaoCompleted);

    proxy.CalcularComissaoAsync("2");
    Console.ReadLine();
}

private static void proxy_CalcularComissaoCompleted(object sender, 
    CalcularComissaoCompletedEventArgs e)
{
    Console.WriteLine("Fim.");
}

Se notarmos a implementação interna do proxy, veremos que o método XXXAsync faz o uso do método InvokeAsync, da classe ClientBase<T>. Este método está disponível somente a partir do .NET Framework 3.5. Sendo assim, alguns detalhes durante a geração do proxy precisam ser analisados:

  • Via “Add Service Reference”: se voce estiver fazendo a referencia em um projeto que esteja utilizando o .NET Framework 3.5 e voce opta pela geração dos métodos que dão suporte ao processamento assíncrono, ele também criará os tipos necessários para suportar o modelo de eventos.

  • Via svcutil.exe: neste caso voce precisará especificar através do parametro /async e, além disso, especificar a versão do .NET Framework através do parâmetro /targetClientVersion, apontando para Version30 ou, se quiser utilizar o modelo baseado em eventos, utilizar a opção Version35.

Processamento Assíncrono no Servidor

Tudo que vimos acima consiste em permitir que o cliente que consome o serviço execute o método de forma assíncrona, evitando que a aplicação não seja bloqueada enquanto o método está sendo executado. A partir de agora iremos analisar como implementar o processamento assíncrono no servidor. Isso consistirá em uma mudança considerável no contrato do serviço e que veremos mais abaixo.

A finalidade do processo assíncrono do lado do servidor é permitir aumentar a escalabilidade e a performance sem afetar os clientes que consomem o serviço. É importante dizer que os processos assíncronos do lado do cliente e do lado do servidor trabalham de forma independente, visando benefícios totalmente diferentes. A idéia do processamento assíncrono do lado do servidor visa principalmente a execução de tarefas que tem um custo alto para execução e, alguns casos comuns, são consultas a alguma informação no banco de dados, leitura/escrita de arquivos no disco, etc.

Isso permitirá a liberação da thread que está executando o serviço seja liberada enquanto o processo custoso acontece em paralelo, permitindo à você executar alguma tarefa em paralelo enquanto o processo ocorre ou até mesmo não ter threads sendo bloqueadas enquanto aguarda este processamento. Mais uma vez, você pode combinar este recurso as técnicas de processamento assíncrono que já existem dentro do .NET Framework, como é o caso do ADO.NET 2.0, classes do namespace IO, etc. Para o exemplo, iremos simular um processo custoso na base de dados.

Como já foi dita acima, o contrato do serviço mudará. Precisaremos desenhá-lo para se enquadrar no padrão de processamento assíncrono do .NET Framework que, como já sabemos, para cada método, teremos na verdade um par onde, o primeiro corresponde ao início da execução (BeginXXX) e, o segundo, corresponde à finalização do processamento (EndXXX). O código abaixo ilustra como devemos proceder para definirmos tais métodos:

using System;
using System.ServiceModel;

[ServiceContract]
public interface ICliente
{
    [OperationContract(AsyncPattern = true)]
    IAsyncResult BeginRecuperar(AsyncCallback callback, object state);

    Cliente[] EndRecuperar(IAsyncResult ar);
}

O código acima ilustra exatamente os passos que devemos seguir para possibilitar a chamada assíncrona pelo WCF. Os métodos que desejamos que sejam invocados assincronamente pelo WCF devem, ao invés de ter apenas um único método para realizar a tarefa, devemos obrigatoriamente dividi-los em duas partes (métodos): BeginXXX e EndXXX. É importante dizer que apenas o método BeginXXX será decorado com o atributo OperationContractAttribute, definindo a propriedade AsyncPattern para True. Ainda seguindo o padrão do processamento assíncrono do .NET Framework, o método Begin deve receber como parâmetro um objeto do tipo AsyncCallback (que mais tarde apontará para o método EndXXX) e um Object, retornando um objeto que implementa a Interface IAsyncResult. Já o método EndXXX deverá efetivamente retornar a informação que o método destina-se a fazer e, como parâmetro, deve receber um objeto que implementa a Interface IAsyncResult.

Quando o contrato acima for exposto pelo WCF e algum cliente consumi-lo, apenas terá um único método chamado Recuperar. Como foi dito anteriormente, a idéia aqui é evitar que o cliente saiba como isso está implementado dentro do serviço. Neste caso, não faz sentido você ter “uma versão síncrona do método” e, caso tenha, por padrão, o WCF sempre irá utilizá-la.

Uma vez que o contrato esteja definido, devemos implementá-lo na classe e, conseqüentemente, expor a mesma para ser consumida. A implementação da Interface ICliente assim como qualquer outra que exige o processamento assíncrono, demanda alguns cuidados no momento da implementação em relação ao formato tradicional. Como estamos utilizando o ADO.NET 2.0 e o mesmo já traz nativamente o suporte assíncrono a execução de queries de forma assíncrona, podemos integrá-lo a execução:

using System;
using System.Threading;
using System.Data.SqlClient;
using System.Collections.Generic;

public class ServicoDeClientes : ICliente
{
    private const string SQL_CONN_STRING = "...;Asynchronous Processing=True";
    private SqlConnection _connection;
    private SqlCommand _command;

    public IAsyncResult BeginRecuperar(AsyncCallback callback, object state)
    {
        this._connection = new SqlConnection(SQL_CONN_STRING);
        this._command = new SqlCommand("SELECT Nome FROM Cliente", this._connection);

        this._connection.Open();
        return this._command.BeginExecuteReader(callback, state);
    }

    public Cliente[] EndRecuperar(IAsyncResult ar)
    {
        List<Cliente> resultado = new List<Cliente>();

        using (this._connection)
            using(this._command)
                using (SqlDataReader dr = this._command.EndExecuteReader(ar))
                    while (dr.Read())
                        resultado.Add(new Cliente() { Nome = dr.GetString(0) });

        return resultado.ToArray();
    }
}

No método que inicia o processo abrimos a conexão com a base de dados, informamos a query e, finalmente, invocamos o método BeginExecuteReader passando o callback e o object que é passado como parâmetro para o método BeginRecuperar. Finalmente, o método EndRecuperar é invocado e através dele coletamos o resultado da execução do processo pesado, preparamos o mesmo e retornamos. Este será o resultado que será encaminhado ao cliente.

O código do lado do cliente não muda em nada. Independentemente da implementação que você utilize do lado do servidor, do lado do cliente nada mudará, ou seja, mesmo que você crie dois métodos (Begin e End) para compor o processamento assíncrono, o WCF sempre disponibilizará um único método para o cliente. Ele poderá invocá-lo de forma síncrona ou assincronamente.

Conclusão: No decorrer deste artigo pudemos entender como integrar o processamento assíncrono em serviços WCF. É possível realizar essa técnica em ambos os lados (cliente e servidor), mas é importante lembrar que os mesmos trabalham de forma independente que, apesar de serem semelhantes, tem finalidade completamente diferente e, sendo assim, necessitamos entender o contexto e aplicar a implementação ideal.

ChamadasAssincronas.zip (211.70 kb)

Async Handler

Desde a versão 1.x do ASP.NET podemos facilmente criar um handler para permitir a execução de algum código. O idéia por detrás deste handler, é justamente servir como alvo para alguma requisição. Um exemplo muito típico é a extração de arquivos do banco de dados, arquivos do disco, geração automática de imagens, etc..

Havia alguns detalhes necessários na versão 1.x que, se não realizados, não funcionava adequadamente. A partir da versão 2.0, a Microsoft incluiu um novo tipo de arquivo, com extensão ASHX, chamado de Generic Handler. Esse arquivo traz uma classe que, por padrão, implementa a interface IHttpHandler, cabendo aos desenvolvedores implementar o método ProcessRequest para tratar o pedido.

O problema é que o processamento deste handler estará ocupando uma thread do ThreadPool que, por sua vez, serve as requisições para as páginas da aplicação. Caso o código a ser realizado dentro deste handler for um processo custoso, podemos comprometer as outras requisições que nada tem a ver com ele (este artigo explica mais detalhes sobre este comportamento).

Para permitir que o handler também faça o uso do processamento assíncrono, podemos fazer com que o mesmo implemente a interface IHttpAsyncHandler e, ao invés de implementar o método ProcessRequest, devemos implementar os métodos BeginProcessRequest e EndProcessRequest. O exemplo abaixo utiliza os métodos (BeginRead e EndRead) para leitura assíncrona de um arquivo do disco:

public class ImageAsyncHandler : IHttpAsyncHandler
{
    private struct InnerState : IDisposable
    {
        public HttpContext HttpContext;
        public FileStream Stream;

        public void Dispose()
        {
            if (this.Stream != null)
                this.Stream.Dispose();
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }

    public IAsyncResult BeginProcessRequest(HttpContext context,
        AsyncCallback cb, object extraData)
    {
        string fileName =
            context.Server.MapPath(context.Request.QueryString[“FileName”]);

        FileStream fs = new FileStream(fileName, FileMode.Open);
        int length = (int)fs.Length;

        return fs.BeginRead(new byte[length], 0, length, cb,
            new InnerState()
            {
                HttpContext = context
                , Stream = fs
            });
    }

    public void ProcessRequest(HttpContext context) { }

    public void EndProcessRequest(IAsyncResult result)
    {
        using (InnerState state = (InnerState)result.AsyncState)
        {
            int length = state.Stream.EndRead(result);
            state.Stream.Position = 0;
           
            byte[] temp = new byte[length];
            state.Stream.Read(temp, 0, length);

            state.HttpContext.Response.OutputStream.Write(temp, 0, length);
        }
    }
}

Esse processo não mudará nada o resultado final do handler, mas trabalhará de uma forma muito mais otimizada, garantido que threads criadas para servir a aplicação não fiquem ocupadas executando um processamento mais custoso. Caso a imagem ou os dados venham de um banco de dados, podemos combinar essa técnica com os métodos assíncronos do ADO.NET 2.0.

ManualResetEvent vs. AutoResetEvent

Ambas as classes que são temas do post tem basicamente a mesma finalidade, ou seja, efetuar a sinalização entre threads. Isso permite que uma determinada thread notifique as outras threads que ela finalizou ou liberou um determinado recurso e, conseqüentemente, essas outras threads poderão dar seqüencia na execução.

Leve em consideração o seguinte código:

delegate void Executor();

private static XXX _resetEvent = new XXX(false);

static void Main(string[] args)
{
    new Executor(Teste1).BeginInvoke(null, null);
    new Executor(Teste2).BeginInvoke(null, null);
    new Executor(Teste3).BeginInvoke(null, null);

    Console.WriteLine(“Fim”);
    Console.ReadLine();
}

static void Teste1()
{
    Console.WriteLine(“Teste1 – Antes”);

    Thread.Sleep(5000); //Simula um processamento pesado
    _resetEvent.Set();
   
    Console.WriteLine(“Teste1 – Depois”);
}

static void Teste2()
{
    Console.WriteLine(“Teste2 – Antes”);
    _resetEvent.WaitOne();
    Console.WriteLine(“Teste2 – Depois”);
}

static void Teste3()
{
    Console.WriteLine(“Teste3 – Antes”);
    _resetEvent.WaitOne();
    Console.WriteLine(“Teste3 – Depois”);
}

Resultados para quando XXX = ManualResetEvent
Fim
Teste1 – Antes
Teste2 – Antes
Teste3 – Antes
Teste1 – Depois
Teste3 – Depois
Teste2 – Depois

Resultados para quando XXX = AutoResetEvent
Fim
Teste1 – Antes
Teste2 – Antes
Teste3 – Antes
Teste1 – Depois
Teste3 – Depois

Como sabemos, o método BeginInvoke fornecido pela instancia do delegate permite a chamado para o método que ele aponta de forma assíncrona, ou seja, será criada uma worker thread para cada um deles; sendo assim, os processos acontecerão paralelamente e o reset event está aqui para garantir a sincronização das informações, ou melhor, destes métodos.

Quando utilizamos o método WaitOne do ManualResetEvent nos métodos 2 e 3, eles aguardarão um sinal que, por sua vez, será dado através do método Set, também do ManualResetEvent. Enquanto isso não acontecer, os métodos 2 e 3 não darão sequencia no processamento. Se repararmos o resultado final, as mensagens (writelines) que a aparecem depois do método WaitOne retornará, isso quer dizer que ele ficará “travado” até receber o sinal para prosseguir. Uma vez recebido, todos aqueles que estiveremos pendentes serão executados.

Já com um AutoResetEvent, o processo é muito semelhante, com a exceção que quando o método Set for chamado, apenas uma thread da fila será executada. Isso explica o motivo pelo qual a mensagem do método 2 (“Teste2 – Depois“) não aparece no resultado final.

Bem, isso é uma das N possibilidades que o .NET fornece para efetuar a sincronização através de sinalização. Para aqueles que querem se aprofundar, vejam o namespace System.Threading.

Duplex Services no ASP.NET

O WCF fornece uma funcionalidade chamada Duplex Services. Este tipo de serviço permite que o servidor invoque um método do lado do cliente através de callbacks.

Quando estamos trabalhando com Windows Forms, o consumo deste tipo de serviço não tem muito segredo, pois voce pode apontar para um método dentro do teu formulário ou até mesmo em uma classe do teu projeto que, quando chegar o momento, o runtime do WCF o invocará. No ASP.NET as coisas funcionam de forma diferente. Uma vez que voce requisita uma página ASPX, o ASP.NET cria o objeto correspondente a mesma, executa e, por fim, o descarta. Com isso, se apontarmos o callback de um serviço duplex para um método qualquer da página, muito provavelmente, quando chegar o momento do serviço executá-lo, ele já não existirá mais.

Para resolver, ou melhor, aguardar até que o callback seja disparado, voce pode utilizar um sincronizador, como o é o caso do ManualResetEvent que irá aguardar até que o callback seja disparado. O exemplo desta utilização é mostrado através do trecho de código abaixo:

public partial class _Default : Page, IServiceCallback
{
    private ManualResetEvent _mre = new ManualResetEvent(false);

    protected void Page_Load(object sender, EventArgs e)
    {
        using (ServiceClient proxy =
            new ServiceClient(new InstanceContext(this)))
        {
            proxy.Send(“Israel”);
            _mre.WaitOne();
        }
    }

    public void Notification(string message)
    {
        Response.Write(message);
        _mre.Set();
    }
}

Como podemos notar, o método WaitOne aguardará até que o método Set seja disparado que, por sua vez, está sendo invocado dentro do método de callback.

Por dentro da Base Class Library

A Base Classe Library (também conhecida como BCL) é um conjunto de classes que o .NET disponibiliza para todas as linguagens que rodam sob o .NET Framework. Essa base encapsula várias funcionalidades que tornam o trabalho dos desenvolvedores muito mais fácil. As classes contidas dentro da BCL é comum para qualquer tipo de aplicação, ou seja, independentemente de tecnologia (ASP.NET, Windows Forns, WPF, etc.), você poderá consumir essas classes que, representam tarefas que são comumente utilizadas. A imagem abaixo exibe onde a BCL está encaixada dentro da plataforma .NET.

Figura 1Base Class Library (BCL).

A versão 2.0 adicionou novas tipos e namespaces, enriquecendo ainda mais esta estrutura. Essas classes vão desde novas coleções (tipadas) até novos namespaces, como é o caso do System.Transactions. Apesar do .NET Framework estar em sua versão 3.5, ele utiliza o .NET 2.0 como seu núcleo. A figura abaixo ilustra perfeitamente a posição do .NET 2.0 dentro do .NET 3.X.

Figura 2 – Gráfico que exibe a posição do .NET 2.0.

A BCL é composta por vários namespaces e, através dos capítulos abaixo, veremos detalhadamente cada um dos principais deles. A idéia é abordar o conteúdo mais útil ao dia-à-dia, mostrando exemplos em Visual Basic .NET e Visual C#. Sendo assim, nem todas as classes/funcionalidades serão cobertas aqui mas, para isso, poderá recorrer ao MSDN Library.

ATENÇÃO: Os arquivos estão em formato XPS. Caso não tenha o visualizador, você poderá baixá-lo aqui.

Conteúdo

  • Capítulo 1 – Tipos de dados e Interfaces Este capítulo abordará a arquitetura de tipos fornecido pelo .NET Framework, onde na primeira parte do capítulo, será abordado os tipos padrões padrões e veremos como identificar se trata-se de um tipo valor ou referência. Além disso, analisaremos um problema grave, deixado de lado por muitos desenvolvedores, que é a questão do boxing e unboxing. Ainda nessa primeira parte, analisaremos alguns novos tipos introduzidos nesta versão do .NET Framework, em principal, os Generics. Na segunda e última parte do mesmo, vamos abordar as várias Interfaces que estão disponíveis para serem implementados em tipos customizados, fornecendo funcionalidades adicionais ao tipo criado.

  • Capítulo 2 – Trabalhando com Coleções As coleções são componentes importantes em qualquer tipo de aplicação e, para isso, esse capítulo abordará extensamente o uso das mesmas, começando pelas coleções primárias, fornecidas desde as primeiras versões do .NET Framework até as novas coleções, introduzidas na versão 2.0 do .NET Framework, quais fazem uso dos Generics. Além das coleções, analisaremos as Interfaces disponíveis para podermos estender as funcionalidades existentes e customizarmos para o nosso cenário.
  • Capítulo 3 – Utilização de Assemblies Um Assembly é a menor unidade de reutilização, segurança e controle de versão. O Assembly é algo importante que deve ser analisado cuidadosamente, pois toda aplicação .NET depois de compilada gerará um Assembly. Este capítulo abordará a sua criação desde um utilitário de linha de comando até o Visual Studio .NET. Além disso, abordaremos também outros assuntos relacionados a Assemblies, como por exemplo, strong names, GAC (Global Assembly Cache), instaladores e arquivos de configuração.
  • Capítulo 4 – Monitoramento e depuração de aplicações Toda e qualquer aplicação necessita de algum tipo de monitoramento de seu código para detectar possíveis problemas que possam acontecer e que devem ser analisados. O .NET Framework fornece várias classes que ajudam nesse monitoramento e, este capítulo, é responsável por apresentar essas classes que vão desde a manipulação do Event Log do Windows até classes que interagem com o WMI – Windows Management Instrumentation.
  • Capítulo 5 – Manipulando o sistema de arquivos Grande parte das aplicações comerciais que temos atualmente manipulam arquivos. Esses arquivos são arquivos de bancos, arquivos de parceiros e fornecedores que servem para troca de informações. Enquanto os XML Web Services ainda não são uma realidade para muitas empresas, a manipulação de arquivos e seus respectivos conteúdos é ainda muito utilizado. Tendo esse cenário, o capítulo em questão abordará as principais classes contidas dentro do namespace System.IO para exemplificar e facilitar a manipulação de arquivos do disco e streams de dados.
  • Capítulo 6 – Serialização A serialização de dados é cada dia mais utilizada em aplicações. Por mais que isso aconteça nos bastidores, esse capítulo abordará desde o seu conceito até como implementá-la; e ainda, em seus diversos formatos, utilizando as classes fornecidas pelo .NET Framework 2.0. Além disso, analisaremos classes e Interfaces que temos disponíveis, que proporcionaram o processo de serialização e deserialização mais flexível, onde podemos customizar e interceptar cada um desses processos de acordo com a nossa necessidade.
  • Capítulo 7 – Globalização de Aplicações Cada vez mais se desenvolve softwares que podem ser acessados por várias pessoas de diferentes idiomas e de diferentes locais do mundo. Tendo esse cenário, é importante que a aplicação que estamos desenvolvendo seja possível ao usuário poder customizar o idioma que deseja visualizar os dados e ainda, poder criar a aplicação independente de qualquer cultura. Essa capítulo tem justamente essa finalidade, ou seja, de exibir o que o .NET Framework é capaz de fazer para atender essa necessidade que, torna-se cada vez mais comum.
  • Capítulo 8 – Criptografia 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 pois, se esses dados cairem em mãos erradas, essas pessoas com más intenções, não consigam entender e/ou recuperar esses dados em sua forma legível. Esse capítulo abordará extensamente as classes responsáveis por criptografia e hashing que o .NET Framework disponiliza, bem como utilizá-las e como aplicá-las ao dia-à-dia.
  • Capítulo 9 – Utilizando Code Access Security – CAS 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. Code Access Security (também conhecido como CAS), é um mecanismo que ajuda limitar/conceder o acesso que o código que está querendo realizar, protegendo recursos e operações. Este capítulo abordará como utilizar o CAS, que é fornecido juntamente com o SDK do .NET Framework e, como configurar devidamente a aplicação para evitar problemas relacionados a segurança.
  • Capítulo 10 – Envio de Mensagens (E-mails) Envio de e-mails é muito comum em qualquer tipo de aplicação, seja ela uma aplicação para internet, uma aplicação para Windows ou até mesmo serviços que rodam sem uma intervenção do usuário. O .NET Framework fornece um namespace contendo classes e muitos outros tipos que podemos utilizar nas aplicação para habilitar o envio de e-mails e, conseqüentemente, torná-las muito mais dinâmicas e inteligentes.
  • Capítulo 11 – Criando Serviços do Windows Os Serviços do Windows (Windows Services), permitem-nos criar aplicações que rodam em “background” no sistema operacional. Estes serviços podem ser automaticamente inicializados quando o sistema operacional inicializar, podendo ainda ser pausado e reinicializado, sem apresentar nenhuma interface com o usuário. Esses serviços são ideais para ser usado em servidores ou em funcionalidades de longa duração que necessitem ser executadas de forma totalmente independente, sem a intervenção de um usuário. O capítulo corrente abordará desde a sua criação, depuração e instalação do mesmo.
  • Capítulo 12 – Interoperabilidade com componentes COM A Microsoft criou a plataforma .NET e, em pouco tempo, essa plataforma foi adotada por muitas e muitas empresas. Algo importante é que muitas dessas empresas, já tinham componentes COM que eram utilizados em massa nas aplicações e que são inviáveis para serem reescritos imediatamente. Felizmente a Microsoft pensou no legado e possibilita a interoperabilidade de componentes COM, interagindo com aplicações baseadas na plataforma .NET e vice-versa. Este capítulo mostrará os passos necessários para efetuar essa interoperabilidade entre as novas aplicações e o que já existe em código legado.
  • Capítulo 13 – Reflection Reflection é a habilidade de extrair informações de metadados de um determinado tipo, ou seja, quais parâmetros, métodos, entre outros membros um determinado tipo possui. Isso torna a aplicação bastante flexível, onde podemos extrair informações necessárias para podermos customizar e automatizar a criação de ferramentas e utilitários que auxiliam os próprios desenvolvedores. Além disso, permite a criação em runtime de Assemblies e como instanciar classes via programação. Esse capítulo propõe-se a explicar como criar esse tipo de funcionalidade dentro da aplicação.
  • Capítulo 14 – Threading A criação de threads permitem aumentar consideravelmente a performance das aplicações. Elas fornecem a habilidade de conseguirmos delegar processamentos em diversas unidades de execução, aumentando a capacidade de processamento de uma aplicação. Mas utilizando isso de forma errada, poderá piorar ao invés de melhorar, consumindo mais recursos do que o necessário, tendo um comportamento inesperado e retornando valores diferentes do esperado. O .NET Framework fornece várias classes que podemos utilizar para criação e gerenciamento de threads, bloqueio de recursos em um ambiente multi-threading e sincronização. Este capítulo irá ajudá-lo a conhecer alguns problemas existentes em aplicações que fazem o uso de threads e como contorná-los.

Referências Bibliográficas

  • Code Complete – Second Edition
    Autor: Steve McConnell
    Editora: Microsoft Press
    ISBN: 0-7356-1967-0
  • Writing Secure Code – Second Edition
    Autores: Michael Howard e David LeBlanc
    Editora: Microsoft Press
    ISBN: 0-7356-1722-8
  • CLR via C# – Second Edition
    Autor: Jeffrey Richter
    Editora: Microsoft Press
    ISBN: 0-7356-2163-2
  • Programming Visual C# 2005: The Language
    Autor: Donis Marshall
    Editora Microsoft Press
    ISBN: 0-7356-2181-0
  • Expressões Regulares – Uma abordagem divertida
    Autor: Aurélio Marinho Jargas
    Editora: Novatec
    ISBN: 85-7522-100-0
  • Collection 5160: Core Development with Microsoft .NET Framework 2.0
    Autor/Editor: Microsoft
  • Collection 5161: Advanced Development with Microsoft .NET Framework 2.0
    Autor/Editor: Microsoft

Recursos da Palestra no TechEd Brasil 2007

Abaixo os arquivos referentes a minha apresentação (Deep Dive no ASP.NET 2.0) que realizei no TechEd Brasil 2007:

  • WEB308_IsraelAece.zip: Apresentação contendo algumas explicações e alguns trechos superficiais das funcionalidades abordadas.
  • TechEd2007.zip: Projeto de exemplo que utilizei para demonstração das funcionalidades abordadas na palestra.

PageAsyncTasks vs. Páginas Assíncronas

Uma pergunta muito pertinente que me fizeram durante a palestra, mais precisamente quando estava falando sobre as páginas assíncronas, é se é ou não possível definir um timeout para a execução da mesma.

Desta forma apenas, não. Para isso, você deve utilizar as chamadas Tarefas Assíncronas. Basicamente ela tem o mesmo conceito das páginas assíncronas, mas permite a definição/suporte de timeout (em segundos), propagação do contexto de segurança para as chamadas assíncronas e, finalmente, a possibilidade de execução paralela das tarefas (pois podemos adicionar N tarefas em cada página).

Abaixo segue a implementação básica desta funcionalidade:

<%@ Page Async=”true” AsyncTimeout=”15″ … %>

protected void Page_Load(object sender, EventArgs e)
{
    this.RegisterAsyncTask(
        new PageAsyncTask(
            new BeginEventHandler(Inicio),
            new EndEventHandler(Fim),
            new EndEventHandler(ExcedeuTimeout),
            null,
            true)); //Processamento paralelo (default é false)

    this.ExecuteRegisteredAsyncTasks();               
}

private IAsyncResult Inicio(object sender, EventArgs e, AsyncCallback callback, object state) { }

private void Fim(IAsyncResult result) { }

private void ExcedeuTimeout(IAsyncResult result) { }

Propagando o contexto de segurança nas chamadas assíncronas

Nas versões anteriores do ASP.NET quando precisamos efetuar um processamento assíncrono através da Interface IHttpAsyncHandler, o método a ser disparado iria correr sob as credenciais do worker process, o que poderia causar problemas de segurança, já que o contexto de segurança da aplicação não era propagado para o processamento assíncrono.

O ASP.NET 2.0 resolve isso com as PageAsyncTasks onde, automaticamente, o contexto é propagado sem que o desenvolvedor precise se preocupar em escrever código para gerenciar isso.

Navegando pelo diretório do .NET Framework 2.0 em %windir%Microsoft.NETFrameworkv2.0 eu vi um arquivo que não conhecia: Aspnet.config. Este arquivo possui um elemento chamado runtime e, dentro dele, dois sub-elementos, a saber: legacyImpersonationPolicy e alwaysFlowImpersonationPolicy.

<?xml version=”1.0″ encoding=”UTF-8″ ?>
<configuration>
    <runtime>
        ……
        <legacyImpersonationPolicy enabled=”true”/>
        <alwaysFlowImpersonationPolicy enabled=”false”/>

    </runtime>
</configuration>

Para que o comportamento seja semelhante as versões anteriores, o elemento legacyImpersonationPolicy é definido como false e, sendo assim, o contexto não será propagado. Caso você não precise manter compartibilidade com aplicações em outras versões, você pode inverter os valores do atributo enabled de ambos elementos e, consequentemente, todo o contexto de segurança será propagado entre as chamadas assíncronas da aplicação, inclusive se utilizando a Interface IHttpAsyncHandler.

RunInUIThread

Ontem enquanto estávamos falando sobre Multithreading em aplicações Windows Forms durante o curso, lembrei-me de um atributo muito interessante que o Roy Osherove criou.

Como sabemos, quando criarmos uma worker thread, voce não pode manipular qualquer controle que está dentro da aplicação, pois os controles só podem ser acessados através da thread que os criou. Sendo assim, o Roy desenvolveu um atributo chamado RunInUIThreadAttribute, que voce coloca no método que será executado pela thread de background. Automaticamente, ao executar esse método e identificar que ele está decorado com o atributo RunInUIThreadAttribute, automaticamente ele se encarregará de encontrar e processar aquele método através da thread correta.

ADO.NET – Trabalho Assíncrono

Temos nas versões 1.x do ADO.NET três métodos bastante importantes dentro da classe XXXCommand, sendo eles: ExecuteReader (retorna um result-set de dados), ExecuteXmlReader (retorna um result-set em formato XML (XmlReader)) e ExecuteNonQuery (executa uma Transact-SQL statement e retorna um número inteiro que representa o número de linhas afetadas). Todos estes métodos são responsáveis por executar de forma síncrona, bloqueando assim a Thread corrente.

O problema deste comportamento é que temos que aguardar o processamento da base de dados finalizar para que possamos novamente ter acesso operante ao sistema. Isso acaba sendo bastante complicado, já que muitas vezes o processo é demorado, principalmente quando as regras de negócio estão dentro da base de dados. Apesar de se conseguir realizar um processo assíncrono no ADO.NET 1.x com o auxílio de Delegates Assíncronos e da classe ThreadPool, a Microsoft decidiu facilitar nesta versão do ADO.NET a criação de comandos assíncronos na base de dados, onde foi adotada uma metodologia similar ao que temos dentro do .NET Framework, mas até o momento é somente suportado pelo provider SqlClient.

Para estes mesmos métodos, temos também mais dois métodos auxiliares (Begin e End) para suportar chamadas assíncronas. A tabela abaixo mostra os novos métodos:

Síncrono Assíncrono (Begin) Assíncrono (End)
ExecuteNonQuery BeginExecuteNonQuery()
BeginExecuteNonQuery(AsyncCallback, Object)
EndExecuteNonQuery(IAsyncResult)
ExecuteReader BeginExecuteReader()
BeginExecuteReader(AsyncCallback, Object)
EndExecuteReader(IAsyncResult)
ExecuteXmlReader BeginExecuteXmlReader()
BeginExecuteXmlReader(AsyncCallback, Object)
EndExecuteXmlReader(IAsyncResult)

Os métodos BeginXXX têm geralmente duas sobrecargas, onde em uma delas não é passado nenhum parâmetro. Já na outra dois parâmetros são requeridos: um delegate do tipo AsyncCallback que apontará para um procedimento de callback, que será executado quando a operação for finalizada; já o segundo parâmetro trata-se de um objeto que é passado para esse procedimento de callback, que poderá ser recuperado pela propriedade AsyncState. Essa propriedade é acessada através do objeto IAsyncResult (retornado pelo método BeginXXX), o qual é passado como parâmetro para o procedimento de callback e, está definido na assinatura do delegate AsyncCallback.

Como já foi dito, esses métodos retornam um objeto do tipo IAsyncResult que representa o status da operação assíncrona. Este, por sua vez, é passado para o procedimento de callback (quando existir), que provê informações importantes e também necessárias para finalizarmos o processo assíncrono. Já os métodos EndXXX devem ser invocados para completar a operação do processo assíncrono. Em seu parâmetro devemos também passar um objeto do tipo IAsyncResult, que será retornado pelo método BeginXXX.

Antes de visualizarmos os exemplos concretos desta funcionalidade, temos uma configuração a ser realizada na ConnectionString que devemos nos atentar para que as operações resultem. Nesta string de conexão com a base de dados, necessitamos definir um parâmetro que indicará ao .NET que esse banco de dados poderá trabalhar com processos assíncronos. Trata-se do parâmetro Asynchronous Processing que, se quisermos em algum momento da aplicação trabalhar de forma assíncrona, devemos definí-la como True. O código abaixo exemplifica como deverá ficar a string de conexão para suportar esta funcionalidade:

"integrated security=SSPI;data source=localhost;
    initial catalog=Northwind;Asynchronous Processing=True"

Já ara exemplificar toda a teoria até o momento, veremos os dois exemplos de operações assíncronas na base de dados, onde, em um dos casos, somente vamos lançar a operação assíncrona para ser realizada. Já no segundo exemplo, utilizaremos uma função de callback. É importante dizer que para simularmos uma operação demorada na base de dados SQL Server, utilizaremos o comando WAITFOR DELAY que, especificado um tempo, ele aguardará pelo mesmo até executar o procedimento que queremos que a base de dados faça. Nos exemplos abaixo estaremos passando para esse comando o valor de 10 segundos. Primeiramente analisaremos a forma de lançar um processo assíncrono na base de dados sem a utilização de callbacks:

using System.Data.SqlClient;
using System.Configuration;

namespace WindowsApplication2
{
    public partial class Form1 : Form
    {
        private SqlCommand _cmd;
        private SqlConnection _conn;
        private IAsyncResult _result;

        private void IniciaProcesso_Click(object sender, EventArgs e) {
            try {
                this._conn = 
                    new SqlConnection(GetConnStringFromConfigFile("ConnString"));
                this._cmd = 
                    new SqlCommand("WAITFOR DELAY '0:0:10'; " + 
                        "INSERT INTO Pedido (Cliente) VALUES ('Israel Aece')", this._conn);

                this._conn.Open();
                this._result = this._cmd.BeginExecuteNonQuery();
            }
            catch {
                this.TextBox1.Text = "Banco de dados indisponível.";
            }
        }

        private void VerificaProcesso_Click(object sender, EventArgs e) {
            try {
                if (!this._result.IsCompleted) {
                    this.TextBox1.Text = "Incompleto.";
                }
                else {
                    this.TextBox1.Text = "Finalizado. " +
                        this._cmd.EndExecuteNonQuery(this._result).ToString() +
                        " registro(s) afetado(s).";

                    this._conn.Close();
                }
            }
            catch (Exception ex) {
                this.TextBox1.Text = "Erro: " + ex.ToString();
                this._conn.Close();
            }
        }
    }
}

Ao clicar no botão “Iniciar Processo”, enviamos para a base de dados o comando a ser executado através do método BeginExecuteNonQuery do objeto SqlCommand que temos dentro do formulário. Depois de realizado isso, verificamos se o processo foi ou não finalizado através da propriedade IsCompleted do objeto IAsyncResult, a qual já vimos a sua funcionalidade um pouco mais acima. Se o processo não foi finalizado, definimos na caixa de texto a mensagem que ainda está em processamento. Do contrário, exibimos a quantidade de registros afetados pela query.

É importante dizer que até que o processo não for finalizado, a conexão com a base de dados não poderá ser fechada. Se notarmos, o método Close do objeto SqlConnection somente é chamado quando a propriedade IsCompleted retornar True, ou quando uma falha na execução da query for encontrada, pois o bloco Catch será disparado. A imagem abaixo ilustra esse processo que descrevemos acima:

Figura 1 – Verificando se o processo foi finalizado através da propriedade IsCompleted.

Callbacks

Ao contrário do exemplo anterior, com a utilização de callbacks temos a vantagem de executarmos algum código quando o processo assíncrono da base de dados for finalizado. Isso tira o encargo do usuário ficar verificando isso, ou seja, quando invocamos qualquer método BeginXXX, temos uma sobrecarga que aceitará um parâmetro do tipo AsyncCallback, que é um Delegate que apontará para um procedimento que será invocado automaticamente quando o processo finalizar. Iremos utilizar o mesmo cenário do exemplo anterior, só que agora com o auxílio dos callbacks. Vejamos o código já implementado:

using System.Data.SqlClient;
using System.Configuration;

namespace WindowsApplication2
{
    public partial class Form2 : Form
    {
        private SqlConnection _conn;

        private void IniciaProcesso_Click(object sender, EventArgs e) {
            try
            {
                this._conn = 
                    new SqlConnection(GetConnStringFromConfigFile("ConnString"));
                SqlCommand cmd =
                    new SqlCommand("WAITFOR DELAY '0:0:10'; " +
                        "INSERT INTO Pedido (Cliente) VALUES ('Israel Aece')", this._conn);

                this._conn.Open();
                AsyncCallback callback = new AsyncCallback(MeuCallback);
                cmd.BeginExecuteNonQuery(callback, cmd);
                this.TextBox1.Text = "Processando...";
            }
            catch {
                this.TextBox1.Text = "Banco de dados indisponível.";
            }
        }

        private void MeuCallback(IAsyncResult result) {
            SqlCommand cmd = (SqlCommand)result.AsyncState;
            this.TextBox1.Text = "Registros afetados: " +
                cmd.EndExecuteNonQuery(result).ToString();

            if (this._conn != null) this._conn.Close();
        }
    }
}

Analisando o código acima, criamos um objeto do tipo SqlConnection e, no evento Click do botão “IniciaProcesso”, atribuímos à instância ao mesmo, recuperando a ConnectionString do arquivo de configuração. Logo em seguida, criamos o objeto SqlCommand, o qual será responsável por criar o comando que será executado na base de dados e, por fim, criamos um delegate do tipo AsyncCallback, onde definimos o procedimento que será executado quando o processo na base de dados for finalizado. Para que o processo seja inicializado, invocamos o método BeginExecuteNonQuery, passando o delegate e o objeto que iremos utilizar no procedimento apontado pelo delegate. Neste caso, iremos mandar para o procedimento o próprio objeto SqlCommand, já que o utilizaremos para encerrar o processo.

Já no procedimento “MeuCallback”, o qual foi definido no delegate para ser processado quando o processo assíncrono for finalizado, reparem que, como parâmetro, recebemos um objeto do tipo IAsyncResult (isso é uma “exigência”, pois o delegate tem essa assinatura), e este por sua vez, fornece uma propriedade chamada AsyncState, a qual retorna um objeto definido pelo usuário, contendo informações sobre a operação assíncrona. Agora, simplesmente fazemos a conversão para o objeto SqlCommand e invocamos o método EndExecuteNonQuery para finalizar o processo assíncrono. É importante que até o retorno deste processo a conexão com a base de dados não pode ser fechada, ou seja, isso somente deverá ser feito quando o processo assíncrono for finalizado.

Figura 2 – Utilizando Callbacks.

Processo Assíncrono e ASP.NET

ASP.NET 2.0 já traz instrinsicamente uma infra-estrutura para trabalharmos com páginas/chamadas assíncronas, simplificando bastante a sua utilização. Isso pode ser usado em conjunto com esta nova funcionalidade de executar assincronamente queries na 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. 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 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(GetConnStringFromConfigFile("ConnString"));
        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();
    }
}

Como as páginas assíncronas, suas funcionalidades e características fogem um pouco do escopo deste artigo, fica aqui uma referência para aqueles que querem entender um pouco mais sobre elas.

ADONET20.zip (118.08 kb)