Criando Indicadores de Progresso Parciais em WinForms com C# e Tasks

No desenvolvimento de aplicações WinForms, é comum encontrar cenários onde certas operações de carregamento de dados ou processamento levam tempo considerável. Se essas operações forem executadas diretamente na thread principal da interface do usuário (UI), a aplicação pode congelar, tonrando-se não responsiva e frustrando o usuário. Para evitar isso, é essencial implementar mecanismos de carregamento assíncrono que mantenham a UI fluida, exibindo um indicador de progresso localizado em um controle específico, em vez de bloquear toda a janela.

Este artigo demonstra como criar uma janela de espera personalizada (um UserControl) em C# para WinForms, utilizando o paradigma de programação assíncrona com Task para realizar carregamentos parciais sem impactar a responsividade da thread principal.

Desenvolvendo o UserControl para Indicação de Carregamento

Vamos começar criando um UserControl que servirá como nosso indicador visual de progresso. Este controle será sobreposto a um elemento pai específico (como um GroupBox ou Panel) durante a execução de uma tarefa demorada, desabilitando o controle pai para evitar interações enquanto o carregamento ocorre.

O UserControl implementará IDisposable para garantir a correta liberação de recursos, incluindo um Timer que atua como um mecanismo de timeout para operações que demoram demais.

Abaixo está a estrutura da classe IndicadorProgressoLocal:


using System;
using System.Drawing;
using System.Windows.Forms;
using System.ComponentModel; // Para DesignerCategory, se necessário

namespace SeuProjetoWinForms
{
    public partial class IndicadorProgressoLocal : UserControl, IDisposable
    {
        private Timer temporizadorExpiracao;
        private bool _disposed = false; // Flag para detectar chamadas redundantes

        public IndicadorProgressoLocal(Control controlePai)
        {
            InitializeComponent(); // Componentes visuais do UserControl

            this.Parent = controlePai;
            this.Parent.Enabled = false; // Desabilita o controle pai enquanto o indicador está ativo
            this.Visible = false; // Inicialmente invisível

            // Configura o temporizador para gerenciar timeouts
            temporizadorExpiracao = new Timer(this.components); 
            temporizadorExpiracao.Interval = 30000; // Define 30 segundos como tempo limite
            temporizadorExpiracao.Tick += new EventHandler(TemporizadorExpiracao_Tick);
        }

        /// <summary>
        /// Inicia a exibição do indicador e o temporizador de expiração.
        /// </summary>
        public void Iniciar()
        {
            this.BringToFront(); // Garante que o indicador esteja sobre o controle pai
            this.Visible = true; // Torna o indicador visível
            temporizadorExpiracao.Enabled = true; // Inicia o temporizador
        }

        /// <summary>
        /// Fecha o indicador e libera os recursos.
        /// </summary>
        public void Fechar()
        {
            // Chama a implementação IDisposable para liberar recursos
            this.Dispose();
        }

        protected override void OnLayout(LayoutEventArgs e)
        {
            base.OnLayout(e);
            PosicionarNoCentro(); // Garante que o indicador esteja centralizado no controle pai
        }

        /// <summary>
        /// Posiciona o UserControl no centro do controle pai.
        /// </summary>
        private void PosicionarNoCentro()
        {
            if (Parent != null)
            {
                int x = (Parent.Width - this.Width) / 2;
                int y = (Parent.Height - this.Height) / 2;
                this.Location = new Point(x, y);
            }
        }

        /// <summary>
        /// Evento de Tick do temporizador, disparado em caso de timeout.
        /// </summary>
        private void TemporizadorExpiracao_Tick(object sender, EventArgs e)
        {
            // Adicionar lógica de log de erro ou notificação de timeout aqui
            Console.WriteLine("Carregamento excedeu o tempo limite e foi encerrado."); 
            temporizadorExpiracao.Stop();
            Fechar(); // Fecha o indicador após o timeout
        }

        /// <summary>
        /// Implementação do padrão IDisposable para liberar recursos.
        /// </summary>
        /// <param name="disposing"></param>Indica se está sendo chamado por um método Dispose explícito.
        protected override void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    // Libera recursos gerenciados
                    if (temporizadorExpiracao != null)
                    {
                        temporizadorExpiracao.Stop();
                        temporizadorExpiracao.Dispose();
                        temporizadorExpiracao = null;
                    }

                    // Reabilita o controle pai se ele ainda estiver válido
                    if (this.Parent != null && !this.Parent.IsDisposed)
                    {
                        this.Parent.Enabled = true;
                    }
                }

                // Libera recursos não gerenciados (se houver)
                // ...

                _disposed = true;
            }
            base.Dispose(disposing); // Chama o Dispose da classe base
        }

        /// <summary>
        /// Sobrescreve o método Dispose público para adicionar a lógica de InvokeRequired.
        /// </summary>
        public new void Dispose() // Usa "new" para ocultar o Dispose da classe base
        {
            if (this.InvokeRequired)
            {
                this.Invoke(new Action(() => Dispose(true)));
            }
            else
            {
                Dispose(true);
            }
            GC.SuppressFinalize(this); // Impede que o finalizador seja chamado
        }
    }
}

Utilizando o Indicador de Carregamento em Operações Assíncronas

Para integrar nosso IndicadorProgressoLocal com operações assíncronas, usaremos a Task Parallel Library (TPL) do .NET. Em particular, Task.Factory.StartNew para iniciar a operação em uma thread separada e ContinueWith para definir ações a serem executadas após a conclusão (ou cancelamento/erro) da tarefa principal.

Também incorporaremos um CancellationTokenSource para permitir o cancelamento de tarefas em andaemnto, o que é crucial para gerenciar múltiplos pedidos de carregamento e evitar condições de corrida.


using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
// Assumindo que IndicadorProgressoLocal está no namespace SeuProjetoWinForms

namespace SeuProjetoWinForms
{
    public partial class MainForm : Form
    {
        private CancellationTokenSource _fonteCancelamentoAtual;
        private Task<indicadorprogressolocal> _tarefaCarregamentoAtual;

        // Exemplo de um controle GroupBox no seu formulário
        private GroupBox meuGroupBoxPrincipal; 

        public MainForm()
        {
            InitializeComponent();
            // Simule a inicialização do GroupBox, ou use um GroupBox real do designer
            meuGroupBoxPrincipal = new GroupBox { Text = "Dados", Dock = DockStyle.Fill };
            this.Controls.Add(meuGroupBoxPrincipal);
        }

        private void BotaoCarregarDados_Click(object sender, EventArgs e)
        {
            IniciarCarregamentoDeDados();
        }

        /// <summary>
        /// Inicia o processo de carregamento de dados com um indicador de progresso.
        /// </summary>
        private void IniciarCarregamentoDeDados()
        {
            // Se uma tarefa de carregamento anterior ainda estiver ativa, cancele-a.
            if (_tarefaCarregamentoAtual != null && 
                (_tarefaCarregamentoAtual.Status == TaskStatus.Running || 
                 _tarefaCarregamentoAtual.Status == TaskStatus.WaitingToRun || 
                 _tarefaCarregamentoAtual.Status == TaskStatus.WaitingForActivation))
            {
                _fonteCancelamentoAtual?.Cancel();
                _tarefaCarregamentoAtual = null; // Limpa a referência da tarefa anterior
            }

            _fonteCancelamentoAtual = new CancellationTokenSource();
            
            // Cria e exibe o indicador de progresso no controle pai desejado.
            IndicadorProgressoLocal indicador = new IndicadorProgressoLocal(this.meuGroupBoxPrincipal); 
            indicador.Iniciar(); 

            // Inicia a tarefa demorada em uma nova thread.
            _tarefaCarregamentoAtual = Task.Factory.StartNew(() =>
            {
                ProcessarDadosAssincronamente(); 
                return indicador; // Retorna o indicador para ser manipulado na continuação.
            }, _fonteCancelamentoAtual.Token);

            // Define uma tarefa de continuação para ser executada após a conclusão (ou falha/cancelamento) da tarefa principal.
            _tarefaCarregamentoAtual.ContinueWith(tarefa =>
            {
                // Garante que o indicador seja fechado e seus recursos liberados.
                if (tarefa.Result != null)
                {
                    tarefa.Result.Fechar(); 
                }

                if (tarefa.IsCanceled)
                {
                    MessageBox.Show("Operação de carregamento foi cancelada.", "Cancelamento", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                else if (tarefa.IsFaulted)
                {
                    // Exibe detalhes do erro, se houver.
                    MessageBox.Show($"Ocorreu um erro durante o carregamento: {tarefa.Exception?.InnerException?.Message}", "Erro", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                else if (tarefa.IsCompleted)
                {
                    MessageBox.Show("Dados carregados com sucesso!", "Concluído", MessageBoxButtons.OK, MessageBoxIcon.Information);
                    // Aqui você atualizaria os controles da UI com os dados carregados.
                    // Exemplo: this.dgvResultados.DataSource = dadosCarregados;
                }
            }, TaskContinuationOptions.AttachedToParent); // A opção AttachedToParent faz com que a continuação seja uma tarefa filha.
        }

        /// <summary>
        /// Simula uma operação de carregamento ou processamento de dados demorada.
        /// </summary>
        private void ProcessarDadosAssincronamente()
        {
            // Esta é a parte que simula o trabalho pesado.
            // IMPORTANTE: Se esta seção precisar interagir com controles da UI,
            // isso deve ser feito via Invoke ou BeginInvoke para garantir segurança de thread.
            Thread.Sleep(5000); // Simula 5 segundos de trabalho.
            
            // Exemplo de atualização de UI segura:
            // if (this.InvokeRequired)
            // {
            //     this.Invoke(new Action(() => {
            //         // SeuControle.Text = "Dados Processados!";
            //     }));
            // }
        }
    }
}
</indicadorprogressolocal>

Otimizando a Chamada com uma Classe Utilitária

Para simplificar o uso do indicador de carregamento em diferentes partes da aplicação, podemos criar uma classe estática utilitária. Esta classe encapsulará a lógica de criação e gerenciamento da tarefa, tornando o processo de exibição do indicador mais limpo e reutilizável.


using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace SeuProjetoWinForms
{
    public static class GerenciadorCarregamentoUI
    {
        private static CancellationTokenSource _fonteCancelamentoEstatica;
        private static Task<indicadorprogressolocal> _tarefaCarregamentoEstatica;

        /// <summary>
        /// Exibe um indicador de carregamento localizado para uma ação assíncrona.
        /// </summary>
        /// <param name="controlePai"></param>O controle WinForms sobre o qual o indicador será exibido.
        /// <param name="acaoCarregamento"></param>A ação a ser executada assincronamente.
        public static void Exibir(Control controlePai, Action acaoCarregamento)
        {
            // Cancela qualquer tarefa de carregamento anterior ainda ativa.
            if (_tarefaCarregamentoEstatica != null && 
                (_tarefaCarregamentoEstatica.Status == TaskStatus.Running || 
                 _tarefaCarregamentoEstatica.Status == TaskStatus.WaitingToRun || 
                 _tarefaCarregamentoEstatica.Status == TaskStatus.WaitingForActivation))
            {
                _fonteCancelamentoEstatica?.Cancel();
                _tarefaCarregamentoEstatica = null;
            }

            _fonteCancelamentoEstatica = new CancellationTokenSource();
            IndicadorProgressoLocal indicador = new IndicadorProgressoLocal(controlePai);
            indicador.Iniciar();

            _tarefaCarregamentoEstatica = Task.Factory.StartNew(() =>
            {
                acaoCarregamento.Invoke(); // Executa a ação fornecida.
                return indicador;
            }, _fonteCancelamentoEstatica.Token);

            _tarefaCarregamentoEstatica.ContinueWith(tarefa =>
            {
                // Fecha o indicador, independentemente do resultado da tarefa.
                if (tarefa.Result != null)
                {
                    tarefa.Result.Fechar();
                }

                if (tarefa.IsFaulted)
                {
                    // Lógica para tratamento de erros, como logar ou exibir uma mensagem genérica.
                    Console.WriteLine($"Erro no carregamento gerenciado: {tarefa.Exception?.InnerException?.Message}");
                }
            }, TaskContinuationOptions.AttachedToParent);
        }
    }
}
</indicadorprogressolocal>

Exemplo de Uso Simplificado

Com a classe utilitária GerenciadorCarregamentoUI, a chamada para exibir um indicador de progresso torna-se muito simples e elegante:


// Em um método de evento do seu formulário, por exemplo:
private void BotaoAtualizarDados_Click(object sender, EventArgs e)
{
    // Supondo que você tem um DataGridView chamado 'dgvDados'
    GerenciadorCarregamentoUI.Exibir(this.dgvDados, () =>
    {
        // Esta é a ação que será executada em segundo plano.
        // Coloque aqui o seu método de carregamento de dados real.
        Thread.Sleep(3000); // Simula um atraso de 3 segundos

        // Se precisar atualizar o dgvDados ou outro controle da UI, use Invoke:
        if (this.dgvDados.InvokeRequired)
        {
            this.dgvDados.Invoke(new Action(() =>
            {
                // Exemplo: this.dgvDados.DataSource = MeuServico.ObterNovosDados();
                // Exemplo: this.labelStatus.Text = "Dados atualizados!";
            }));
        }
    });
}

Tags: C# WinForms Task Parallel Library Asynchronous Programming UI Responsiveness

Publicado em 7-8 06:01