Não tem nada mais frustrante para o usuário quando este precisa realizar alguma operação e o programa simplesmente não responde: não se sabe se o sistema simplesmente travou ou se ainda está realizando a operação. Aqueles mais afoitos não pensam duas vezes e já vão teclando o Ctrl+Alt+Del para finalizar a tarefa. O grande problema disso é que podem ocorrer danos de integridade aos dados da aplicação, daí já vem o usuário solicitar o suporte e reparação dos problemas. Tudo por que o desenvolvedor deixou de aplicar um recurso simples e eficaz, capaz de diminuir jconsideravelmente os esforços de suporte e reparo: a barra de progresso.
Parece algo simples, mas certamente em algum momento de suas carreiras, muitos programadores já perderam algum tempo ao tentar implementar uma barra de progresso de forma eficiente.
O uso da barra é basicamente informativo e tem a função de mostrar para o usuário da aplicação que uma determinada tarefa está sendo executada corretamente. Seja para exibir o progresso de upload/download de dados, leitura de um extenso arquivo XML ou qualquer outra tarefa que demande tempo em sua execução. No caso de transmissão de dados ou a leitura de algum arquivo qualquer, é possível o programador mensurar qual a real progressão da tarefa e, nesse contexto, poderá incrementar a barra de progresso conforme o status da transmissão ou o status da leitura do arquivo.
Em outros casos é quase impossível prever qual a real progressão da tarefa, podendo o programador implementar uma barra de progresso no formato de progressão indeterminada.
De forma resumida, o uso da ProgressBar é uma forma respeitosa de informar ao usuário que o procedimento é demorado, que o programa não travou e que a tarefa está sendo executada corretamente.
O Visual Studio possui uma ferramenta especialmente desenvolvida para casos em que se exige a execução de duas tarefas concomitantes: o backGroundWorker. Podemos exibir as operações da thread da interface do usuário como a barra de progresso em movimento, por exemplo, enquanto tiramos proveito do backGroundWorker para realizar uma tarefa importante em segundo plano, como mostra a Figura 1.
Dicas de quando usar uma ProgressBar
- Não utilizamos barras de progresso em processos que demore pouquíssimos segundos para finalizar, pois não há produtividade nenhuma nisso;
- Em uma tarefa que existe muitos processos conhecidos, evite criar uma barra de progresso para cada processo. Prefira sempre criar apenas uma barra de progresso para toda a tarefa. Várias causam a mesma sensação de que o programa nunca vai parar de ser executado, pois o usuário nunca vai saber quantas barras de progresso serão executadas até a operação ser realmente terminada. Se preferir, crie duas barras de progresso, sendo uma para a tarefa principal e outra para cada processo contidos nela;
- É importante que coloquemos os valores do progresso de acordo com a evolução real da execução do processo. Nunca valores utilizar valores aleatórios, o que causa um efeito nada profissional da aplicação;
- Caso não seja conhecido o final do processo, utilizamos uma barra de status na modalidade indeterminada e, caso a tarefa seja muito demorada, o programador pode se utilizar de um truque simples para informar que os procedimentos estão sendo realizados usando uma simples label com o status da realização de um dos processos contidos na tarefa.
Mãos à obra
Nesse exemplo vamos utilizar um formulário com três buttons, uma progressbar, duas labels e dois componentes backgroundWorker, conforme ilustra a Figura 2.
Vamos criar o método que irá simular uma tarefa real muito longa, que em tese demandaria muito tempo para ser terminada, como mostra a Listagem 1.
/// <summary>
/// Simula uma tarefa que levará muito tempo para ser executada.
/// </summary>
/// <param name="tempo"> Um inteiro que representa o tempo a ser esperado.</param>
private void TarefaLonga(int p)
{
for (int i = 0; i <= 10; i++)
{
// faz a thread dormir por "p" milissegundos a cada passagem do loop
Thread.Sleep(p);
label2.Text = "Tarefa: " + i.ToString() + " comcluída";
}
}
Utilizando a ProgressBar para um processo com fim definido
Pressupõe-se, neste caso, que é sabido quando o processo irá terminar. Exemplo: a leitura das linhas de um arquivo de texto onde se saiba o número total de linhas. Nesse caso, basta configurar a propriedade Maximum da progressbar para corresponder o número de total de linhas do arquivo e incrementar o progresso conforme as linhas são lidas.
Para isso configure o backgroundWorker1 conforme se segue:
Propriedades do componente backgroundWorker1\
Iremos habilitar as propriedades WorkerReportProgress e WorkerSupportsCancellation, conforme a Figura 3. A primeira propriedade basicamente relata o status da execução da tarefa, enquanto que a segunda propriedade oferece suporte ao cancelamento da operação.
Eventos do componente backgroundWorker1
Vamos dar um duplo clique nos seguintes eventos do componente backgroundWorker1, como mostra a Figura 4.
Isso vai gerar os respectivos métodos para a manipulação de uma tarefa em segundo plano. (Não vamos aqui entrar no mérito dos “Threading” em tarefas Assíncronas).
É no evento DoWork que chamamos a tarefa ou as tarefas demoradas a serem executadas em segundo plano. É aqui que vamos chamar o método TarefaLonga().
No evento ProgressChanged é que a mudança do progresso é incrementada ou decrementada e faremos nossa barra de progresso se mover aqui.
Por último, no evento RunWorkerCompleted é que verificamos que fim levou a operação. Se foi cancelada, se ocorreu algum erro ou se tudo ocorreu conforme o esperado. É aqui que programamos o que deve acontecer em cada um dos casos, ou seja, como o sistema vai se comportar após a operação.
Abaixo, na Listagem 2, temos os métodos gerados pelos eventos.
/// <summary>
/// //Aqui chamamos os nossos metodos com as tarefas demoradas.
/// </summary>
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i <100; i++)//representa uma tarefa com 100 processos.
{
//Executa o método longo 100 vezes.
TarefaLonga(20);
//incrementa o progresso do backgroundWorker
//a cada passagem do loop.
this.backgroundWorker1.ReportProgress(i);
//Verifica se houve uma requisição para cancelar a operação.
if (backgroundWorker1.CancellationPending)
{
//se sim, define a propriedade Cancel para true
//para que o evento WorkerCompleted saiba que a tarefa foi cancelada.
e.Cancel = true;
//zera o percentual de progresso do backgroundWorker1.
backgroundWorker1.ReportProgress(0);
return;
}
}
//Finalmente, caso tudo esteja ok, finaliza
//o progresso em 100%.
backgroundWorker1.ReportProgress(100);
}
/// <summary>
/// Aqui implementamos o que desejamos fazer enquanto o progresso
/// da tarefa é modificado,[incrementado].
/// </summary>
private void backgroundWorker1_ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
//Incrementa o valor da progressbar com o valor
//atual do progresso da tarefa.
progressBar1.Value = e.ProgressPercentage;
//informa o percentual na forma de texto.
label1.Text = e.ProgressPercentage.ToString() + "%";
}
/// <summary>
/// Após a tarefa ser concluida, esse metodo e chamado para
/// implementar o que deve ser feito imediatamente após a conclusão da tarefa.
/// </summary>
private void backgroundWorker1_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
//caso a operação seja cancelada, informa ao usuario.
label2.Text = "Operação Cancelada pelo Usuário!";
//habilita o Botao cancelar
btnCancelar.Enabled = true;
//limpa a label
label1.Text = string.Empty;
}else if (e.Error !=null)
{
//informa ao usuario do acontecimento de algum erro.
label2.Text = "Aconteceu um erro durante a execução do processo!";
}else{
//informa que a tarefa foi concluida com sucesso.
label2.Text = "Tarefa Concluida com sucesso!";
}
//habilita os botões.
btnTarefaDeterminada.Enabled = true;
btnTarefaIndeterminada.Enabled = true;
}
Chamando a tarefa de forma Assíncrona
Dê um duplo clique no botão “Rodar Processo” e implemente o código da Listagem 3.
private void btnTarefaDeterminada_Click(object sender, EventArgs e)
{
//desabilita os botões enquanto a tarefa é executada
btnTarefaIndeterminada.Enabled = false;
btnTarefaIndeterminada.Enabled = false;
//define o stilo padrao do progressbar
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.Value = 0;
//executa o processo de forma assincrona.
backgroundWorker1.RunWorkerAsync();
}
Observe que na Listagem 3 chamamos toda a execução da tarefa de forma assíncrona através do método RunWorkerAsync(), que é o gatilho que dispara toda a operação.
Agora vamos dar um duplo clique no botão “Cancelar” e implementar o código da Listagem 4.
private void btnCancelar_Click(object sender, EventArgs e)
{
//Cancelamento da tarefa com fim determinado [backgroundWorker1]
if (backgroundWorker1.IsBusy)//se o backgroundWorker1 estiver ocupado
{
// notifica a thread que o cancelamento foi solicitado.
// Cancela a tarefa DoWork
backgroundWorker1.CancelAsync();
}
//desabilita o botão cancelar.
btnCancelar.Enabled = false;
label1.Text = "Cancelando...";
}
Nesta listagem, o método CancelAsync() é chamado solicitando o cancelamento da operação.
Executando o Programa
Ao executar o programa e clicar no botão “Rodar Processo” veremos um horrível erro de execução, como segue a Figura 5 abaixo.
Isso ocorre por que estamos tentando acessar um processo contido em uma thread diferente da thread que está sendo executada. Para corrigir isso invocamos o processo utilizando o método BeginInvoke daquele processo. Simplificando, o BeginInvoke() executa simultaneamente procedimentos entre threads diferentes.
Vamos alterar o código do método TarefaLonga() conforme a Listagem 5.
private void TarefaLonga(int p)
{
for (int i = 0; i <= 10; i++)
{
// faz a thread dormir por "p" milissegundos a cada passagem do loop
Thread.Sleep(p);
label2.BeginInvoke(
new Action(() =>
{
label2.Text = "Tarefa: " + i.ToString() + " comcluída";
}
));
}
}
Executamos novamente o programa e ao clicar em “Rodar Processo”, temos a Figura 6.
Ao Cancelar Temos a Figura 7.
E ao concluir tem-se a Figura 8.
Utilizando progressBar para executar uma tarefa de conclusão não definida
Quando não se sabe ao certo qual o ritmo de progressão de uma tarefa, ou não se sabe quanto tempo ela vai demorar para ser finalizada utilizamos a progressBar em modo indeterminado. A barra de progresso não irá progredir, apenas exibirá uma animação que mostra que o programa não travou.
Para isso, definimos a propriedade ProgressBarStyle do componente progressBar para marquee. Além disso, precisamos configurar as seguintes propriedades:
Propriedades do componente bgWorkerIndeterminada
Habilite as seguintes propriedades, conforme a Figura 9.
Neste caso, apenas o suporte ao cancelamento deve ser ativado, já que a barra de progresso não será incrementada.
Eventos do componente bgWorkerIndeterminada
Dê um duplo clique para gerar os métodos dos seguintes eventos, presentes na Figura 10.
Observe que o evento ProgressChanged não foi implementado. Neste caso, esse evento não é necessário, pois não carregará a barra de progresso.
Vamos implementar os métodos gerados, como mostra a Listagem 6.
private void bgWorkerIndeterminada_DoWork(object sender, DoWorkEventArgs e)
{
//executa a tarefa a primeira vez
TarefaLonga(100);
//Verifica se houve uma requisição para cancelar a operação.
if (bgWorkerIndeterminada.CancellationPending)
{
//se sim, define a propriedade Cancel para true
//para que o evento WorkerCompleted saiba que a tarefa foi cancelada.
e.Cancel = true;
return;
}
//executa a tarefa pela segunda vez
TarefaLonga(500);
if (bgWorkerIndeterminada.CancellationPending)
{
//se sim, define a propriedade Cancel para true
//para que o evento WorkerCompleted saiba que a tarefa foi cancelada.
e.Cancel = true;
return;
}
}
private void bgWorkerIndeterminada_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
//Caso cancelado...
if (e.Cancelled)
{
// reconfigura a progressbar para o padrao.
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.Value = 0;
//caso a operação seja cancelada, informa ao usuario.
label2.Text = "Operação Cancelada pelo Usuário!";
//habilita o botao cancelar
btnCancelar.Enabled = true;
//limpa a label
label1.Text = string.Empty;
}
else if (e.Error != null)
{
//informa ao usuario do acontecimento de algum erro.
label2.Text = "Aconteceu um erro durante a execução do processo!";
// reconfigura a progressbar para o padrao.
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.Value = 0;
}
else
{
//informa que a tarefa foi concluida com sucesso.
label2.Text = "Tarefa Concluida com sucesso!";
//Carrega todo progressbar.
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.Value = 100;
label1.Text = progressBar1.Value.ToString() + "%";
}
//habilita os botões.
btnTarefaDeterminada.Enabled = true;
btnTarefaIndeterminada.Enabled = true;
}
Podemos observar que na codificação apresentada na implementação do evento DoWork, chamamos o método TarefaLonga() por duas vezes: isso foi realizado propositalmente com fins didáticos para que o leitor compreenda que uma tarefa, após ser iniciada, não pode ser cancelada. O DoWork inicia e termina um procedimento e, logo após, inicia e termina o outro até terminar toda a thread. Nesse intervalo o DoWork verifica se existe um pedido de cancelamento pendente, em caso positivo, o sistema deixa de executar as demais tarefas.
Assim, caso o programador julgue que o usuário possa, em algum momento, cancelar a operação, o programador pode chamar o cancelamento entre as tarefas, o que dá certo controle ao programador, o que evita, dentre outros problemas, a inconsistência e fragmentação de dados do aplicativo. E ainda, se necessário, o desenvolvedor pode reverter as alterações até então realizadas por meio de codificação no evento RunWorkerCompleted, caso a operação seja efetivamente cancelada pelo usuário.
Dê um duplo clique no botão “Rodar Processo com Finalização Indeterminada” e implemente o seguinte código da Listagem 7.
private void btnTarefaIndeterminada_Click(object sender, EventArgs e)
{
//desabilita os botões enquanto a tarefa é executada.
btnTarefaDeterminada.Enabled = false;
btnTarefaIndeterminada.Enabled = false;
bgWorkerIndeterminada.RunWorkerAsync();
//define a progressBar para Marquee
progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.MarqueeAnimationSpeed = 5;
//informa que a tarefa esta sendo executada.
label1.Text = "Processando...";
}
Como podemos observar, modificamos o style da progressBar para Marquee e também configuramos a velocidade da animação para 5, que é uma animação bem lenta.
Edite o código do botão “Cancelar” conforme a Listagem 8.
private void btnCancelar_Click(object sender, EventArgs e)
{
//Cancelamento da tarefa com fim determinado [backgroundWorker1]
if (backgroundWorker1.IsBusy)//se o backgroundWorker1 estiver ocupado
{
// notifica a thread que o cancelamento foi solicitado.
// Cancela a tarefa DoWork
backgroundWorker1.CancelAsync();
}
//Cancelamento da tarefa com fim indeterminado [bgWorkerIndeterminada]
if (bgWorkerIndeterminada.IsBusy)
{
// notifica a thread que o cancelamento foi solicitado.
// Cancela a tarefa DoWork
bgWorkerIndeterminada.CancelAsync();
}
//desabilita o botão cancelar.
btnCancelar.Enabled = false;
label1.Text = "Cancelando...";
}
Executando o programa
Execute o programa e clique no botão “Rodar Processo com Finalização Indeterminada”. Veremos o mesmo resultado da Figura 11.
Ao concluir a tarefa tem-se a Figura 12.
Ao clicar em Cancelar tem-se a Figura 13.
Atenção! A atomicidade da operação depende exclusivamente da programação do desenvolvedor. Lembrando que uma vez iniciada uma tarefa dentro do DoWork ela será concluída. O cancelamento consiste em não executar as demais tarefas do processo como um todo, no momento que método CancelAsync() é invocado. Mais uma vez, depende do programador reverter ou não as modificações até então realizadas.
Para quê complicar? De fato o que parece ser algo simples é realmente muito simples de ser feito, o que não deixa de ser trivial. Da próxima vez que precisarmos implementar uma barra de progresso, utilizaremos a solução nativa do Visual Studio, o backGroundWorker. Uma solução bem simples, rápida, com satisfação garantida do usuário final e, o melhor de tudo, com todos os recursos e vantagens do .NET Framework.