domingo, 22 de junho de 2008

Programação Assíncrona [Parte 2 - BackgroundWorker]

Em .NET existe um componente que provê uma forma bastante simplificada de se realizar as diversas operações necessárias para se executar código de forma assíncrona:
  • Exibir o progresso da operação;

  • Obter o resultado na thread que iniciou a execução assíncrona;

  • Cancelar a operação;

  • Monitorar o estado da operação.


Este componente é o BackgroundWorker (System.ComponentModel.BackgroundWorkder).

Para aplicações WindowsForms adicionar este componente é tão simles quanto realizar um drag and drop do mesmo na parte Components da Toolbox do Visual Studio, para as demais aplicações

é necessário declarar e instanciar um objeto BackgroundWorker, o que é também um trabalho trivial.

Um vez que temos um BackgroundWorker em mãos vamos ao que nos interessa. Primeiramente vamos supor que o código que desejamos executar de forma assíncrona é o representado pelo método BaixeArquivos, abaixo:


private void BaixeArquivos(string[] urls)

{

foreach (string url in urls)

{

BaixeArquivos(url, DiretorioDeDestino);

}

}

Obs.: Considere também que BaixeArquivo é um método que realmente realiza o download de um dado arquivo, com base na url do mesmo, e que DiretorioDeDestino é uma constante da aplicação.

Para que este código seja executado via BackgroundWorker é necessário invoca-lo de dentro do evento DoWork:

BackgroundWorker worker = new BackgroundWorker(); // variável de instância

void AlgumMetodo()

{

//codigo qualquer

worker.DoWork += (Worker_DoWork);

worker.RunWorkerAsync(

new string[]

{

"http://solucoesageis.com.br/Portals/0/logoNFe.JPG",

"http://www.google.com.br/intl/pt-BR_br/images/logo.gif",

"http://www.msdnbrasil.com.br/anp/img/tit_ranking_msdn.gif"

});

//codigo qualquer

}

void Worker_DoWork(object sender, DoWorkEventArgs e)

{

BaixeArquivos(e.Argument as string[]);

}

O código acima já ilustra duas operações do componente em questão: a criação do método que irá executar de forma assíncrona(assinar o evento DoWork) e o disparo da operação (invocar o método RunWorkerAsync). Note que os argumentos necessários para a execução da operação são passados atravéz do argumento do método RunWorkerAsync, que é do tipo object, e depois deve ser convertido para o tipo apropriado dentro do código que executa a operação.

Neste ponto já sabemos criar um BackgroundWorker, assinar o seu evento principal fornecer os argumentos necessários, iniciar a operação, obter os argumentos(via DoWorkEventArgs.Argument) e realizar o trabalho necessário. Para situções mais simples isso já é o suficiente, porém se o caso for esse aconselho o uso de delegates, pois se trata do caso mais simples de execução assíncrona.

Então o que mais pode ser necesário ??

  • Relatar Progresso: para relatar progresso é necessário assinar o evento ProgressChanged e invocar o método ReportProgress. Veja a aplicação desta estratégia em nosso estudo de caso:


void AlgumMetodo()

{

//codigo qualquer

worker.DoWork += (Worker_DoWork);

worker.ProgressChanged += (Worker_ProgressChanged);

worker.RunWorkerAsync(

new string[]

{

"http://solucoesageis.com.br/Portals/0/logoNFe.JPG",

"http://www.google.com.br/intl/pt-BR_br/images/logo.gif",

"http://www.msdnbrasil.com.br/anp/img/tit_ranking_msdn.gif"

});

//codigo qualquer

}

void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)

{

Console.Write("Operação " + e.ProgressPercentage + "% concluída.");

}

private void BaixeArquivos(string[] urls)

{

int cont = 0;

foreach (string url in urls)

{

BaixeArquivos(url, DiretorioDeDestino);

worker.ReportProgress((++cont)/urls.Length);

}

}

Veja que o método que relata o progresso é o que assina o evento ProgressChanged e que o método ReportProgess deve ser invocado de dentro do código que é executado assíncronamente, pois este deve saber em qual porcentagem de conclusão ele se encontra.

Outro detalhe é a propriedade WorkerReportsProgress que indica se o componente irá invocar o evento ProgressChanged quando o método ReportProgress for chamado, o valor dessa propriedade é true por default.

Pronto agora já sabemos como relatar progresso, o controle ProgressBar poderia ser usado no lugar da escrita no Console para aplicações WindowsForms. Então vamos à próxima necessidade:

  • Cancelar a operação: para cancelar uma operação que foi iniciada por um BackgroundWorker é necessário invocar o método CancelAsync que irá disparar o evento RunWorkerCompleted. Quando este método é invocado o propriedade CancellationPending é configurada com true, e então o valor dessa variável deve ser verificado dentro do método que assina o evento DoWork para que neste a lógica de cancelamento seja executada. Assim como no relatório de progresso você tem a opção de desabilitar o cancelamento da operação atravéz da porpriedade WorkerSuportsCancellation, que é true por default. Veja a ilustração de um cancelameno abaixo:


void AlgumMetodo()

{

//codigo qualquer

worker.DoWork += (Worker_DoWork);

worker.ProgressChanged += (Worker_ProgressChanged);

worker.RunWorkerCompleted += (Worker_RunWorkerCompleted);

worker.RunWorkerAsync(

new string[]

{

"http://solucoesageis.com.br/Portals/0/logoNFe.JPG",

"http://www.google.com.br/intl/pt-BR_br/images/logo.gif",

"http://www.msdnbrasil.com.br/anp/img/tit_ranking_msdn.gif"

});

//codigo qualquer

}

private void VerificaSeEhNecessarioCancelar()

{

// Verificou-se que é necessário cancelar

worker.CancelAsync();

}

void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

{

if (e.Cancelled)

Console.WriteLine("Operação cancelada");

}

private void BaixeArquivos(string[] urls)

{

int cont = 0;

foreach (string url in urls)

{

if (worker.CancellationPending)

{

// Possível lógica de cancelamento deve estar aqui

e.Cancel = true;

return;

}

BaixeArquivos(url, DiretorioDeDestino);

worker.ReportProgress((++cont)/urls.Length);

}

}

Observe que a lógica de cancelamento deve ser inserida no código que está sendo executado de forma assíncrona. Além disso note que evento para este caso é o evento de conclusão da operação, isto é, ele será invocado não só apenas quando houver cancelamento da operação, e sim sempre que esta for conlcuída, independentemente do resultado. Com base nisto vamos para o próximo requisito:

  • Obter o resultado da operação: como você já sabe assinar o evento que será disparado no momento de conclusão da operação resta apenas um detalhe: indicar o resultado, isto é feito atravéz da propriedade DoWorkEventArgs.Result. Como a propriedade Result só pode ser obtida de dentro do método que assina o evento DoWork teremos que migrar o código do método BaixeArquivos para dentro do método Workder_DoWork (claro que existem outras formas de se fazer isto, mas vamos simplificar). Vamos ao código:


void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

{

if (e.Cancelled)

Console.WriteLine("Operação cancelada");

else

Console.WriteLine("Resultado: " + e.Result);

}


void Worker_DoWork(object sender, DoWorkEventArgs e)

{

string urls = e.Argument as string[];

int cont = 0;


foreach (string url in urls)

{

if (worker.CancellationPending)

{

// Possível lógica de cancelamento deve estar aqui

e.Cancel = true;

return;

}

BaixeArquivos(url, DiretorioDeDestino);

worker.ReportProgress((++cont)/urls.Length);

}


e.Result = "Downalods Realizados";

}

A propriedade Result é do tipo Object, o que permite que qualquer tipo de resultado seja retornado atravéz dela.

A última manipulação que será explicada é a consulta do status de uma operação assíncrona.

  • Requisitando o status de uma operação: Em algum momento de seu código pode ser necessário saber se a operação está acontecendo, ou se ela já está concluída (pois nem sempre o resultado da mesma será retornado), resumindo, pode ser necessário saber se o componente BackgroundWorker está trabalhando ou se ele está livre. Exemplo: para poder iniciar uma nova operação, no caso desta operação não poder ser executada mais de uma vez ao mesmo tempo. Para saber isso é preciso consultar a propriedade IsBusy do BackgroundWorker. Código:


private void TenteLancarOperacao()

{

if (!worker.IsBusy)

{

worker.RunWorkerAsync(

new string[]

{

"http://solucoesageis.com.br/Portals/0/logoNFe.JPG",

"http://www.google.com.br/intl/pt-BR_br/images/logo.gif",

"http://www.msdnbrasil.com.br/anp/img/tit_ranking_msdn.gif"

});

}

}


Espero que neste ponto você considere um novo componente para sua caixa de ferramentas de programação. Existem mil formas de se fazer a mesma coisa, e tudo indica que devemos dar preferência para o caminho mais curto e o BackgroundWorker é um caminho curto para operações assíncronas onde é necessário um acompanhamento minucioso do processo, pois neste a parte burocrática já está pronta, basta inserir o código certo no local certo.


Até o próximo post!

2 comentários:

Rui Lopes da Silva disse...

Obrigado pela explicação. ajudou me mt

. disse...

Parabéns amigo.
Artigo bem escrito e lucidativo sobre o BackGroundWorker. É raro de encontrar em lingua portuguesa escritores que desenvolvam suas idéias e não simplesmente copiam e traduzem idéias de outros.