Web Parts Assíncronas
Fritz Onion
Conteúdo
· Congestionamento de Web Part
· Acesso à Web Assíncrono
· Como funciona
· Acesso a Dados Assíncrono
Construir um Web site completo personalizável com uma coleção de Web Parts plugáveis é bastante fácil com a infra-estrutura de portal do ASP.NET 2.0. Este modelo é muito flexível, permitindo aos usuários colocar facilmente nossos Web Parts em qualquer lugar da Página Web com total liberdade para personalizar o nosso site. No entanto, estas vantagens também podem conduzir a problemas de eficiência, podendo até degradar a experiência do usuário, pois não podemos conhecer com antecedência quais componentes serão utilizados em conjunto, e conseqüentemente, não poderemos fazer otimizações de recuperação de dados específicas para cada componente individual.
A problema de eficiência mais comum em um sítio de portal típico ocorre quando múltiplas Web Parts fazem solicitações de dados em rede em simultaneamente. Cada solicitação, seja para um Web service ou para um banco de dados remoto, acaba aumentando o tempo total necessário para processar a página, mesmo quando as solicitações são tipicamente independentes uma da outra e poderiam ser, de modo concebível, emitidas em paralelo.
Felizmente, o ASP.NET 2.0 introduz também um modelo de página assíncrono de fácil manuseio que, quando utilizado em combinação com chamadas de Web service assíncronas e acesso a banco de dados assíncrono, pode melhorar significativamente o tempo de resposta da página de portal, ao permitir que várias Web Parts independentes coletem dados em paralelo. Veremos aqui, técnicas para construir Web Parts que executam a recuperação de dados assincronamente, para tornar páginas de portal que as contêm, mais responsivas e escaláveis.
Congestionamento de Web Parts
Comecemos considerando a página de portal mostrada na Figura 1. Neste exemplo há quatro Web Parts em uma página de portal, cada uma recuperando dados de uma fonte diferente. A fonte completa desta aplicação exemplo, está disponível para o download no Web site da MSDN®Magazine e recomendamos rever a aplicação enquanto estiver lendo este artigo. No exemplo, três das Web Parts recuperam os seus dados de um Web service, o qual intencionalmente espera durante três segundos antes de retornar. A quarta Web Part emite uma consulta ADO.NET para um banco de dados SQL Server, o qual também espera três segundos antes de retornar. Este é um exemplo exagerado do problema, porém, não é totalmente improvável.

Figura 1 - Página de Portal de Exemplo
Cada uma das Web Parts na aplicação exemplo, foi construída com um controle de usuário e associa os resultados da recuperação de dados para o controle que os exibir. O código e o markup de cada controle são mantidos em um mínimo, para que o exemplo seja simples e nos permita concentrar-nos em construir as Web Parts assíncronas.
Eis um arquivo de controle de usuário NewsWebPart.ascx:
<%@ Control Language="C#" AutoEventWirp="true" CodeFile="NewsWebPart.ascx.cs" Inherits="webparts_ NewsWebPart" %> <asp:BulletedList ID="_newsHeadlines" runat="server"> </asp:BulletedList>
E este é o correspondente código por trás (codebehind) para o arquivo de títulos de notícias do Web Part de exemplo:
public partial class webparts_NewsWebPart : UserControl{ protected void Page_Load(object sender, EventArgs e) { PortalServices ps = new PortalServices(); _newsHeadlines.DataSource = ps.GetNewsHeadlines(); _newsHeadlines.DataBind(); }}
Observe como interage com um Web service para recuperar os news headlines (títulos de notícias) de exemplo. A Web Part de stock quotes (cotação de ações) e a Web Part do weather forecaster (previsão de tempo) são implementados de forma muito parecida, porém utilizam métodos diferentes do mesmo Web service para recuperar os seus dados. Semelhantemente, a Figura 2 mostra o arquivo de controle de usuário SalesReportWebPart.ascx e o arquivo de código por trás correspondente o relatório comercial do Web Part de exemplo. Observar que o controle utiliza o ADO.NET para recuperar os dados comerciais de um banco de dados e, a seguir, povoa um controle GridView com estes dados.

Figura 3 - Processamento Seqüencial de Web Part
Assim que a página de portal exemplo for rodada, um problema se torna evidente: o processamento da solicitação leva mais de 12 segundos - uma demora que fará com que a maior parte dos usuários deixe de utilizar a aplicação. A razão deste longo atraso é mostrada na Figura 3, onde observamos o rastreamento do caminho de execução que a solicitação percorre, quando esta página é executada. Como qualquer outro controle na hierarquia de controles de página, cada Web Part é carregada por sua vez, na ordem definida pela hierarquia de controle de página. Como este processo é seqüencial, cada Web Part deve esperar que a parte que a precede na hierarquia, tenha finalizado o seu processamento, antes que possa começar a solicitar os seus dados e servir a sua resposta. Por causa desses três segundos de atraso artificialmente introduzidos em cada recuperação de dados, vemos claramente por que a resposta leva 12 segundos para ser completada: cada Web Part está executando uma recuperação de dados completamente independente, uma após a outra. O mais importante a ser percebido, é que todas essas recuperações poderiam ser executadas em paralelo, cortando o tempo de resposta em 75 por cento. Este é o nosso objetivo aqui.
Acesso à Web Assíncrono
No exemplo, três Web Parts utilizam Web services para recuperar os seus dados, e cada um utiliza o ADO.NET para acessar um banco de dados. Comecemos tornando as solicitações de Web services assíncronas, já que existe bom suporte nas classes proxy de Web service geradas pela ferramenta Web Services Description Language WSDL.exe (ou no Visual Studio 2005, na ferramenta Visual Studio 2005 Add Web Service Reference) para executar assincronamente invocações de métodos Web.
Quando uma classe proxy de Web service for criada no ASP.NET 2.0, gerará de fato três formas diferentes de invocar um determinado método: uma síncrona e duas assíncronas. Por exemplo, a proxy de Web service que as Web Parts utilizam tem os seguintes métodos disponíveis para invocar métodos Web GetNewsHeadlines:
public string[] GetNewsHeadlines() public IAsyncResult BeginGetNewsHeadlines( AsyncCallback callback, object asyncState) public string[] EndGetNewsHeadlines( IAsyncResult asyncResult) public void GetNewsHeadlinesAsync() public void GetNewsHeadlinesAsync( object userState) public event GetNewsHeadlinesCompletedEventHandler GetNewsHeadlinesCompleted;
O primeiro método, GetNewsHeadlines, é o método síncrono padrão. Os dois seguintes, BeginGetNewsHeadlines e EndGetNewsHeadlines, podem ser utilizados para invocar o método assincronamente e podem ser amarrados a qualquer número de mecanismos assíncronos .NET, através da interface padrão IAsyncResult.
Porém, o método mais interessante para utilizar neste cenário é o último: GetNewsHeadlinesAsync. Para utilizar este determinado método, devemos registrar um delegado com o evento da classe proxy que foi especificamente gerado para capturar os resultados de invocações assíncronas (o evento GetNewsHeadlinesCompleted, no exemplo). A assinatura delegada é fortemente tipada para conter os valores de retorno do método para que possamos facilmente extrair os resultados na implementação do método.
Utilizar este método assíncrono baseado em evento, reescrevendo a invocação de métodos Web na Web Part de headline news (títulos de notícias) de modo assíncrono é fácil, como mostrado na Figura 4. Em primeiro lugar, designamos [RHZ1] um proxy para o evento GetNewsHeadlinesCompleted da classe proxy , e a seguir chamamos o método GetNewsHeadlinesAsync. Na implementação do método subscrito ao evento completado, associamos os resultados da chamada do método Web à lista com bolinhas, para exibi-los ao cliente. Uma consideração adicional é que estes métodos assíncronos só funcionam se a Web Part for colocada na página configurando o atributo Async = "true", o qual pode ser verificado programaticamente, observando-se a propriedade IsAsync da página hospedeira. Se a página em que a Web Part for colocada não for async, então teremos que recorrer à associação síncrona padrão, como mostrado na Figura 4.
Agora, para que a Web Part assíncrona execute a recuperação de dados assincronamente, deve ser colocada na página com o atributo Async configurado como true, portanto modificamos a diretiva Page da página do portal da seguinte forma:
Uma vez atualizadas as outras duas Web Parts que utilizam o Web service para recuperar os seus dados assincronamente, a página de portal se tornará muito mais responsiva. Na verdade, dependendo da ordem em que as partes forem carregadas, poderão ser exibias ao cliente em aproximadamente três segundos (se a Web Part comercial for carregada em primeiro lugar, levará aproximadamente seis segundos!). Embora a Web Part do relatório comercial ainda esteja acessando o banco de dados seqüencialmente, as outras três Web Parts estão executando agora as suas invocações de Web service assincronamente, portanto a thread de solicitação primária não estará mais esperando pela sua realização. Naturalmente, desejamos fazer com que todo o trabalho I/O-bound seja realizado de modo assíncrono, para que os clientes possam utilizar tanto Web services quanto os Web Parts orientados a banco de dados, sem bloqueios seqüenciais desnecessários.
Outra razão para forçar o I/O-bound a funcionar com solicitações de entrada-saída assíncronas, é para permitir com que a thread primária retorne ao pool de threads para servir a outras solicitações. Atualmente, estamos liberando a thread, somente depois que a nossa consulta ao banco de dados de relatórios comercial estiver completa, a que significa que ficamos ociosos durante três segundos, ocupando uma thread do threadpool, que poderia ser utilizada para servir a outras solicitações. Se pudermos tornar também assíncrona esta última solicitação de dados I/O-bound, nossa pagina utilizará a thread de solicitação apenas o tempo suficiente para fazer o spool de todas as solicitações de entrada-saída assíncronas e, a seguir, a devolverá imediatamente para o pool de threads.
Como Funciona
Se já tivermos alguma vez trabalhado com programação assíncrona, provavelmente teremos a sensação que estas pequenas modificações realizadas para a invocação dos Web services, não podem ser suficientes. Nem precisamos mexer com a interface IAsyncResult, nem precisamos avisar à página hospedeira, que estávamos executava operações assíncronas (via registro de uma tarefa ou alguma outra técnica) e, mesmo assim, tudo parece ''funcionar'' como esperado.
O secreto reside em que o Web service encaminha a implementação da classe do método assíncrono, junto com uma classe auxiliar introduzida no Microsoft® .NET Framework 2.0, chamada AsyncOperationManager. Quando chamamos o método GetNewsHeadlinesAsync da classe proxy, acontece um mapeamento da chamada para um método auxiliar interno da classe base SoapHttpClientProtocol, chamado InvokeAsync, do qual a classe por proxy deriva. O InvokeAsync faz duas coisas importantes: registra a operação assíncrona chamando o método estático CreateOperation do AsyncOperationManager e a seguir lança a solicitação assincronamente, utilizando o método BeginGetRequestStream da classe WebRequest. Neste ponto a chamada retorna e a página continua processando seu ciclo de vida, porém, do momento em que a página foi marcada com o atributo Async = "true", só continuará processando a solicitação até o disparo do evento PreRender e, a seguir retornará a thread da solicitação para o pool de threads. Uma vez que a solicitação de Web assíncrona for completada, invocará o método designado [RHZ2] ao evento completado da proxy, em um thread separado obtido do pool de threads de entrada-saída. Se esta for a última operação assíncrona a ser completada (rastreada pelo contexto de sincronização do AsyncOperationManager), a página será chamada e a solicitação completará o seu processamento no ponto em que tinha sido interrompida, começando no evento PreRenderComplete. A Figura 5 mostra o ciclo de vida completo, quando utilizamos solicitações Web assíncronas no contexto de página assíncrona.

Figura 5 - Solicitações Web Assíncronas em Páginas Assíncronas
A classe AsyncOperationManager, foi projetada para ser utilizada em ambientes diferentes, para auxiliar na gerência de invocações de método assíncronas. Por exemplo, se chamamos um Web service assincronamente através de uma aplicação Windows® Forms, este também estaria associada à classe AsyncOperationManager. A diferença entre cada ambiente é o SynchronizationContext associado com AsyncOperationManager. Quando rodamos no contexto de uma aplicação baseada em ASP.NET, o SynchronizationContext será configurado para uma instância da classe AspNetSynchronizationContext. O objetivo principal aqui, é manter um registro de quantas solicitações assíncronas suportadas estão pendentes, para que todas sejam completadas e o processamento da solicitação de página possa prosseguir. Por outro lado, quando em uma aplicação Windows baseada em formulário, o SynchronizationContext será configurado para uma instância da classe WindowsFormsSynchronizationContext. O objetivo principal aqui, é utilizar a melhor forma de subordinar as invocações de uma thread de background em relação à thread da UI.
Acesso de Dados Assíncrono
Agora, voltemos ao problema de tornar a última Web Part assíncrona e à questão geral de executar a recuperação de dados assíncrona com o ADO.NET. Infelizmente, não há nenhum mecanismo assíncrono simples equivalente ao exposto pelo proxy de Web service, que permita executar a recuperação de dados assíncrona, portanto teremos um pouco mais de trabalho para fazer com que a Web Part final seja também assíncrona. Podemos trabalhar com os novos métodos assíncronos da classe SqlCommand e com as funcionalidades de tarefa assíncrona do ASP.NET. Utilizando o SqlCommand, podemos agora invocar comandos assincronamente, via um dos métodos seguintes:
· IAsyncResult BeginExecuteReader(AsyncCallback ac, object state)
· IAsyncResult BeginExecuteNonQuery(AsyncCallback ac, object state)
· IAsyncResult BeginExecuteXmlReader(AsyncCallback ac, object state)
E podemos invocar os métodos de realização correspondentes uma vez que o stream de dados está pronto para começar a ler:
· SqlDataReader EndExecuteReader(IAsyncResult ar)
· int EndExecuteNonQuery(IAsyncResult ar)
· XmlReader EndExecuteXmlReader(IAsyncResult ar)
Para utilizar qualquer um destes métodos de recuperação assíncronos, o "async=true" deve ser acrescentado ao string de conexão. Neste cenário, estamos interessados no povoamento de um GridView associando-o a um SqlDataReader, portanto utilizaremos o método BeginExecuteReader para iniciar a chamada assíncrona.
Para amarrarmos isto a páginas assíncronas, o ASP.NET 2.0 também nos permite registrar tarefas assíncronas que tem de ser executadas antes de que a página complete a exibição. Este é um modelo mais explícito do que o utilizado com o proxy Web service, porém, também fornece mais flexibilidade. Para registrar uma tarefa assíncrona, criamos uma instância da classe PageAsyncTask e a inicializamos com três delegados: begin handler (manipulador de início), end handler (manipulador de fim), and timeout handler (manipulador de estouro de tempo). O begin handler deve retornar uma interface IAsyncResult, portanto é aqui onde será lançada a solicitação assíncrona dos nossos dados utilizando o BeginExecuteReader. O end handler, é chamado assim que a tarefa for completada (neste exemplo, quando já existem dados prontos para serem lidos), no ponto em que podemos utilizar os resultados. O ASP.NET cuidará da invocação do begin handler justamente antes de abandonar a thread de solicitação (imediatamente depois que o evento PreRender for completado). A Figura 6, mostra a implementação atualizada da Web Part de relatório comercial, que executa o acesso de dados assíncrono, utilizando as tarefas assíncronas e o método BeginExecuteReader assíncrono da classe SqlCommand.
Observar que podemos utilizar esta mesma técnica com as nossas solicitações Web service, utilizando os métodos assíncronos alternativos fornecidos na classe proxy (BeginGetNewsHeadlines, por exemplo). Uma vantagem potencial para esta técnica é que podemos também especificar um timeout handler. Se as invocações remotas não conseguirem retornar a tempo, o timeout handler associado será invocado. Este intervalo é especificado na diretiva Page, utilizando o atributo AsyncTimeout, o qual por padrão, é de 20 segundos. Também observar que diferentemente de quando utilizamos o modelo assíncrono baseado em eventos, ao utilizar a Page.RegisterAsyncTask, não teremos que desviar para uma invocação síncrona baseada no resultado de Page.IsAsync. As tarefas de página assíncronas nos Web Parts funcionam perfeitamente em páginas síncronas e inclusive levam em conta a execução paralela de Web Parts. A diferença básica reside em que para páginas síncronas (sem o atributo Async = "true"), a thread da página principal não será liberada de volta para o pool de threads durante a execução das operações assíncronas.
Com todas as Web Parts executando agora a recuperação de dados assincronamente, podemos utilizá-las em qualquer página marcada como assíncrona e ter certeza de que o tempo de resposta para a recuperação dos dados, não será mais a soma dos tempos individuais de todas Web Parts, porém, o tempo máximo necessário para que a Web Part mais demorada conclua o seu processamento. Marcando a página como assíncrona e utilizando as Web Parts que executam entrada-saída assíncrona, também aumentamos a escalabilidade potencial do site, desde que página liberará o thread de solicitação primário para que possa servir a outros clientes que estão esperando pelos dados. O ponto chave aqui, é que se formos construir sites de portal com o ASP.NET 2.0, devemos ter em mente todas as novas funcionalidades assíncronas introduzidas neste lançamento e tirar proveito dos mesmos para melhorar as nossas aplicações tanto em responsividade quanto em escalabilidade. Para obter mais informações sobre o suporte assíncrono no ASP.NET 2.0, ver o artigo Wicked Code do Jeff Prosise, na edição de Outubro de 2005 da Revista MSDN.
Enviar perguntas e comentários ao Fritz para xtrmasp@microsoft.com.
Fritz Onion é co-fundador da Pluralsight, um provedor de treinamento Microsoft .NET, onde chefia o currículo de desenvolvimento em Web. Fritz é o autor de Essencial ASP.NET (Addison Wesley, 2003) e do próximo lançamento Essencial ASP.NET 2.0 (Addison Wesley, 2006).
Figura 2 – Web Part de Relatório de Vendas
SalesReportWebPart.ascx<%@ Control Language="C#" AutoEventWireup="true" CodeFile="SalesReportWebPart.ascx.cs" Inherits="webparts_SalesReportWebPart" %> Sales data for<asp:TextBox ID="_yearTextBox" runat="server" Width="120px">2005</asp:TextBox><asp:Button ID="_setYearButton" runat="server" Text="Set year" /><br /> <asp:GridView ID="_salesGrid" runat="server" AutoGenerateColumns="False" DataKeyNames="id"> <Columns> <asp:BoundField DataField="id" HeaderText="id" Visible="False" ReadOnly="True" /> <asp:BoundField DataField="quarter" HeaderText="Quarter" /> <asp:BoundField DataField="amount" HeaderText="Amount" DataFormatString="{0:c}" HtmlEncode="False" /> <asp:BoundField DataField="projected" HeaderText="Projected" DataFormatString="{0:c}" HtmlEncode="False" /> </Columns></asp:GridView>SalesReportWebPart.ascx.cspublic partial class webparts_SalesReportWebPart : UserControl { protected void Page_Load(object sender, EventArgs e) { string dsn = ConfigurationManager. ConnectionStrings["salesDsn"].ConnectionString; string sql = "WAITFOR DELAY ‘00:00:03’ " + "SELECT [id], [quarter], [year], [amount], "[projected] FROM [sales] WHERE year=@year"; using (SqlConnection conn = new SqlConnection(dsn)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@year", int.Parse(_yearTextBox.Text)); conn.Open(); _salesGrid.DataSource = cmd.ExecuteReader(); _salesGrid.DataBind(); } } }}
Figura 4 – Associação de Dados Assíncronos
public partial class webparts_NewsWebPart : UserControl { protected void Page_Load(object sender, EventArgs e) { // Instanciar o proxy Web service para recuperacao dos dados PortalServices ps = new PortalServices(); // Se a pagina tem o async habilitado, invocar o Web service // assincronamente e colher os resultados no callback. if (Page.IsAsync) { ps.GetNewsHeadlinesCompleted += new GetNewsHeadlinesCompletedEventHandler( ps_GetNewsHeadlinesCompleted); ps.GetNewsHeadlinesAsync(); } else { // Se async no for true, realizar o direct databinding _newsHeadlines.DataSource = ps.GetNewsHeadlines(); _newsHeadlines.DataBind(); } } // Este callbaxk só é invocado se o modelo async web service // foi utilizado. void ps_GetNewsHeadlinesCompleted(object sender, GetNewsHeadlinesCompletedEventArgs e) { _newsHeadlines.DataSource = e.Result; _newsHeadlines.DataBind(); }}
Figura 6 - Acesso a Dados Assíncronos
public partial class webparts_SalesReportWebPart : UserControl { // Variáveis locais para armazenar connection e command // para recuperação de dados async SqlConnection _conn; SqlCommand _cmd; protected void Page_Load(object sender, EventArgs e) { string dsn = ConfigurationManager. ConnectionStrings["salesDsn"].ConnectionString; dsn += ";async=true"; // Permitir operações assincronas string sql = "WAITFOR DELAY ‘00:00:03’ SELECT [id], " + "[quarter], [year], [amount], [projected] FROM " + "[sales] WHERE year=@year"; _conn = new SqlConnection(dsn); _cmd = new SqlCommand(sql, _conn); _conn.Open(); _cmd.Parameters.AddWithValue("@year", int.Parse(_yearTextBox.Text)); // Lançar solicitações de dados assincronamente utilizando a tarefa // de página async Page.RegisterAsyncTask(new PageAsyncTask( new BeginEventHandler(BeginGetSalesData), new EndEventHandler(EndGetSalesData), new EndEventHandler(GetSalesDataTimeout), null, true)); } IAsyncResult BeginGetSalesData(object src, EventArgs e, AsyncCallback cb, object state) { return _cmd.BeginExecuteReader(cb, state); } void EndGetSalesData(IAsyncResult ar) { _salesGrid.DataSource = _cmd.EndExecuteReader(ar); _salesGrid.DataBind(); _conn.Close(); } void GetSalesDataTimeout(IAsyncResult ar) { // A operação caiu por tempo, portanto, apenas encerrar // fechando a conexão if (_conn.State == ConnectionState.Open) _conn.Close(); _messageLabel.Text = "Query timed out..."; } }