quarta-feira, 20 de abril de 2011

Event-based Asynchronous Pattern (EAP). O futuro padrão para as interfaces das aplicações?

Sabe quando seu aplicativo vai realizar uma tarefa demorada e ele “para de responder” enquanto não termina de processar? Esse é o cenário de grande parte dos aplicativos atualmente.

Com a versão 2.0 do Microsoft .Net Framework foi introduzido um pattern endereçado a esse tipo de cenário chamado Event-based Asynchronous Pattern (EAP), oferecendo as vantagens de uma aplicação multithreaded e "escondendo" a complexidade inerente a esse tipo de design.

Com ele, você pode:

  • Executar tarefas demoradas, como downloads e operações de banco de dados, em "background", sem congelar sua aplicação;
  • Executar operações simultaneamente e receber notificações quando cada uma for concluída;
  • Aguardar que recursos fiquem disponíveis sem congelar sua aplicação;
  • Comunicar-se com as operações assíncronas pendentes usando o modelo de eventos;

A efeito de exemplo e aprendizado, o cenário que vamos trabalhar é a leitura de arquivos texto para importação de dados no sistema.

Começamos a implementação do pattern criando as classes de argumento dos eventos que criaremos para nos comunicarmos com o chamador. Essas classes devem herdar do AsyncCompletedEventArgs ou ProgressChangedEventArgs (no caso de informação referente ao progresso da execução).

No nosso caso, a classe padrão (ProgressChangedEventArgs) é suficiente para notificarmos o progresso da leitura e mapeamento dos dados do arquivo. Então, o argumento para a notificação da finalização do processamento ficará:

public class MapCompletedEventArgs : AsyncCompletedEventArgs
{
  public IEnumerable<Customer> CustomersList { get; private set; }
  public MapCompletedEventArgs(IEnumerable<Customer> customersList,
  Exception error, bool isCancelled, object userState)
    : base(error, isCancelled, userState)
  {
    this.CustomersList = customersList;
  }
}

O próximo passo é criar os handles (delegates) para as operações de avisar o progresso e quando o processamento estiver terminado:

public delegate void MapProgressChangedEventHandler(ProgressChangedEventArgs e);
public delegate void MapCompletedEventHandler(MapCompletedEventArgs e);

Agora, criaremos os respectivos eventos para informar o progresso e quando a execução estiver concluída:

public event MapProgressChangedEventHandler MapProgressChanged;
public event MapCompletedEventHandler MapCompleted;

Para garantir o uso da thread correta na execução dos eventos precisamos criar callbacks para serem executados via AsyncOperation:

private SendOrPostCallback onProgressReportDelegate;
private SendOrPostCallback onCompletedDelegate;

A última variável que precisamos é um dicionário para controlar as múltiplas requisições:

private HybridDictionary userStateToLifetime = new HybridDictionary();

No construtor da classe inicializamos os callbacks:

public CustomerMapper()
{
  onProgressReportDelegate = new SendOrPostCallback(ReportMapProgress);
  onCompletedDelegate = new SendOrPostCallback(ContentFileMappingCompleted);
}

private void ContentFileMappingCompleted(object operationState)
{
  OnMapCompleted(operationState as MapCompletedEventArgs);
}

private void OnMapCompleted(MapCompletedEventArgs e)
{
  if (MapCompleted != null)
  {
    MapCompleted(e);
  }
}

private void ReportMapProgress(object state)
{
  OnMapProgressChanged(state as ProgressChangedEventArgs);
}

private void OnMapProgressChanged(ProgressChangedEventArgs e)
{
  if (MapProgressChanged != null)
  {
    MapProgressChanged(e);
  }
}

E finalmente chegamos aos métodos de mapeamento assíncrono do arquivo, o qual, criamos uma operação assíncrona através da classe AsyncOperationManager:

public IAsyncResult MapAsync(string filePath)
{
  var asyncOp = AsyncOperationManager.CreateOperation(filePath);

  lock (userStateToLifetime.SyncRoot)
  {
    if (!userStateToLifetime.Contains(filePath))
    {
      userStateToLifetime[filePath] = asyncOp;
    }
  }

  var action = new Action<string, AsyncOperation>(MapFileWorker);
  return action.BeginInvoke(filePath, asyncOp, null, null);
}

Disponiblizaremos um método para ser utilizado caso o usuário queira cancelar a operação a qualquer momento:

public void CancelReadAsync(string filePath)
{
  var asyncOp = userStateToLifetime[filePath] as AsyncOperation;
  if (asyncOp != null)
  {
    lock (userStateToLifetime.SyncRoot)
    {
      userStateToLifetime.Remove(filePath);
    }
  }
}

No método que realiza o processamento da requisição precisamos garantir que o evento referente a finalização do processamento seja executado em caso de sucesso, cancelamento ou falha. E para executarmos o evento utilizamos o método PostOperationComplete da operação assíncrona que criamos:

private void MapFileWorker(string filePath, AsyncOperation asyncOp)
{
  Exception error = null;
  IEnumerable<Customer>customersList = null;
  if (!TaskCanceled(asyncOp.UserSuppliedState))
  {
    try
    {
      customersList = MapFileLogic(filePath, asyncOp);
    }
    catch (Exception ex)
    {
      error = ex;
    }
  }

  if (!TaskCanceled(asyncOp.UserSuppliedState))
  {
    lock (userStateToLifetime.SyncRoot)
    {
      userStateToLifetime.Remove(asyncOp.UserSuppliedState);
    }
  }

  var arg = new MapCompletedEventArgs(customersList, error,
    TaskCanceled(asyncOp.UserSuppliedState), asyncOp.UserSuppliedState);

  asyncOp.PostOperationCompleted(onCompletedDelegate, arg);
}

Para o evento de acompanhamento do progresso usamos o método Post da operação assíncrona:

var arg = new ProgressChangedEventArgs(progressPercentage, asyncOp.UserSuppliedState);
asyncOp.Post(this.onProgressReportDelegate, arg);

Ao final, a classe que utilizará o método assíncrono ficará assim:

var mapper = new CustomerMapper();
mapper.MapCompleted += new CustomerMapper.MapCompletedEventHandler(mapper_MapCompleted);
mapper.MapProgressChanged += new CustomerMapper.MapProgressChangedEventHandler(mapper_MapProgressChanged);
mapper.MapAsync(Environment.CurrentDirectory + @"\CustomerTextFile1.txt");

E talvez você esteja se pergutando “Legal, e por que eu deveria me importar com isso?”.

Essa é uma tendência de programação que ganhou força com o Silverlight e parece ser o padrão para se programar no Windows Phone 7.

Será que esse pattern se tornará padrão para programação de aplicações no futuro?

Você já usou esse padrão?

Até a próxima!

DOWNLOAD EXEMPLO:
EAP.TEXTFILEMAPPER.RAR


Artigos Recomendados:

>>   Software para Pessoas do Séc XXI
>>   Prepare-se para o C# 5 – Parte 2
>>   Prepare-se para o C# 5 – Parte 1
>>   Reflection de Alta de Performance
>>   Extension Methods = Manutenibilidade

Nenhum comentário:

Postar um comentário