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!";
}));
}
});
}