Introdução à Gestão de Estado em Modais Complexos
A criação de fluxos interativos em aplicações Blazor frequentemente enfrenta desafios relacionados ao gerenciamento de estado quando múltiplas janelas modais são utiilzadas. Aninhamentos profundos podem resultar em perda de contexto, dificuldade na transmissão de dados entre instâncias e falhas na sincronização da interface do usuário. A biblioteca MudBlazor oferece um subsistema robusto baseado em Material Design para contornar essas barreiras, permitindo o controle fino sobre a pilha de diálogos e garantindo tipagem forte nos dados retornados.
Este guia explora mecanismos internos para estruturar fluxos multipasso, implementar navegação dentro de contextos modais e assegurar que os dados trafeguem com integridade através dos serviços de injeção de dependência.
Compreendendo o Núcleo do Sistema de Diálogos
O sistema é sustentado pela interação entre três elementos principais: o serviço injetável IDialogService, o provedor de renderização MudDialogProvider e o componente de container. O modelo segue uma arquitetura orientada a eventos, onde cada nova instância aberta é registrada em uma coleção interna mantida pelo provedor.
A estrutura subjacente atua como uma pilha LIFO (Last-In, First-Out), gerenciando o ciclo de vida das aberturas e fechamentos.
Estrutura de Controle de Instâncias
Dentro do provedor, uma lista armazena as referências ativas. Cada chamada para exibição adiciona um registro, e o fechamento remove essa entrada, disparando atualizações de estado na interface.
// Lógica simplificada do gerenciador interno
private readonly List<IDialogReference> _referenciasAtivas = [];
private Task RegistrarReferencia(IDialogReference instancia)
{
_referenciasAtivas.Add(instancia); // Push na pilha
return InvokeAsync(StateHasChanged);
}
private void RemoverReferencia(IDialogReference instancia, DialogResult? resultado)
{
if (!instancia.Dismiss(resultado)) return;
_referenciasAtivas.Remove(instancia); // Pop da pilha
StateHasChanged();
}
Padrões de Operação com Janelas Modais
Cadeia Sequencial de Interação
Um caso comum envolve a abertura de uma segunda janela baseada na resposta da primeira. O serviço permite aguardar o resultado antes de prosseguir.
@inject IDialogService ServiceDeDialogos
<MudButton OnClick="AbrirSecuencia" Variation="@Variant.Outlined">Processar Fluxo</MudButton>
@code {
private async Task AbrirSecuencia()
{
var paramsIniciais = new DialogParameters { ["CodigoOperacao"] = "001" };
var janelaInicio = await ServiceDeDialogos.ShowAsync<FormularioDados>(
"Coleta Inicial",
paramsIniciais
);
var saidaJanela1 = await janelaInicio.Result;
if (!saidaJanela1.Canceled && saidaJanela1.Data is string idReferencia)
{
var janelaDetalhes = await ServiceDeDialogos.ShowAsync<FormularioEdicao>(
"Edição Detalhada",
new DialogParameters { ["IdOrigem"] = idReferencia }
);
await janelaDetalhes.Result;
SnackbarService.Success("Fluxo de coleta finalizado");
}
}
}
Substituição de Contexto
Em vez de empilhar novas camadas, pode-se substituir o diálogo atual por outro. Isso evita o acúmulo visual desnecessário.
private async Task TrocarContextoModal()
{
var dialogoAtual = await ServiceDeDialogos.GetReferences().LastOrDefaultAsync();
if (dialogoAtual != null)
{
// Fecha a janela ativa enviando um sinal de troca
dialogoAtual.Close(DialogResult.Ok("substituicao"));
// Inicia o novo contexto imediatamente
var novaJanela = await ServiceDeDialogos.ShowAsync<TelaAlternativa>("Nova Visão");
await novaJanela.Result;
}
}
Controle de Restrições de Fechamento
Para operações críticas, desabilite comportamentos padrão de fechamento garantindo que a ação seja confirmada explicitamente.
var configuracaoRestrita = new DialogOptions
{
BackdropClick = false,
CloseOnEscapeKey = false,
MaxWidth = MaxWidth.Small,
CloseButton = true
};
await ServiceDeDialogos.ShowAsync<AvisoSeguranca>(
"Operação Irreversível",
new DialogParameters { ["Destino"] = "Conta Principal" },
configuracaoRestrita
);
Gestão Segura de Dados Retornados
A comunicação entre o diálogo e o chamador deve ser estritamente tipada para evitar exceções de tempo de execução durante o conversão de dados.
Mecanismo Padrão
O objeto DialogResult ancapsula tanto o status da operação quanto o payload. O desenvolvedor acessa isso através da referência devolvida pelo serviço.
// Definição estrutural
public readonly struct ResultadoDialogo
{
public bool Cancelado { get; }
public object? Payload { get; }
public static ResultadoDialogo OK() => new(false, null);
public static ResultadoDialogo OK<T>(T conteudo) => new(false, conteudo);
public static ResultadoDialogo NaoOk() => new(true, null);
}
Extensões para Tipagem Genérica
Crie extensões para facilitar a obtenção direta de objetos fortes, eliminando verificações manuais repetitivas.
public static class ExtensoesDialogo
{
public static async Task<T?> LerComo<T>(this IDialogReference referencia)
{
var resposna = await referencia.Result;
if (resposna.Canceled || resposna.Payload == null)
return default;
return resposna.Payload is T dado ? dado : throw new InvalidCastException();
}
}
// Uso prático
var modal = await ServiceDeDialogos.ShowAsync<CadastroUsuario>("Novo Usuário");
var usuarioCadastrado = await modal.LerComo<ViewModelUsuario>();
if (usuarioCadastrado != null)
{
await RepositorioUsuarios.Atualizar(usuarioCadastrado);
}
Encapsulamento de Objetos Complexos
Para formulários que envolvem múltiplos campos, utilize records ou classes DTOs para transportar o estado.
<MudDialog>
<DialogContent>
<MudTextField @bind-Value="_modelo.Nome" Label="Nome Completo" />
<MudSwitch @bind-Value="_modelo.Ativo" Label="Ativo" />
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancelar">Voltar</MudButton>
<MudButton Color="Color.Success" OnClick="Salvar">Confirmar</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] public MudDialogInstance ReferenciaInstancia { get; set; }
private ModeloEditar _modelo = new();
private void Salvar()
=> ReferenciaInstancia.Close(_modelo);
private void Cancelar()
=> ReferenciaInstancia.Cancel();
}
Indicadores de Ação Multi-opção
Quando um diálogo possui vários botões de ação distintos, retorne um Enum para identificar claramente a intenção do usuário.
public enum AcaoUsuario { Confirmar, Arquivar, Descartar }
// Dentro do Componente
private void ExecutarAcao(AcaoUsuario acao)
=> DialogInstance.Close(acao);
// Consumidor
var dialogRef = await DialogService.ShowAsync<MenuDecisao>("Gerenciar Item");
var escolha = await dialogRef.LerComo<AcaoUsuario>();
_ = escolha switch
{
AcaoUsuario.Confirmar => ProcessarItem(),
AcaoUsuario.Arquivar => MovimentarParaHistorico(),
AcaoUsuario.Descartar => LimparCache(),
_ => Task.CompletedTask
};
Cenários Avançados de Implementação
Assistente Multipasso (Wizard)
Apilhe diálogos sequencialmente onde cada etapa valida os dados da anterior antes de permitir a continuidade.
public async Task IniciarWorkflowCompleto()
{
var etapa1 = await DialogService.ShowAsync<PassoDadosBasi>("Etapa 1: Cadastro");
var dadosEtapa1 = await etapa1.LerComo<DtoBasico>();
if (dadosEtapa1 == null) return Abortar();
var etapa2 = await DialogService.ShowAsync<PassoPreferencias>("Etapa 2: Configuração");
var dadosEtapa2 = await etapa2.LerComo<DtoPreferencias>>(new DialogParameters { ["BaseData"] = dadosEtapa1 });
if (dadosEtapa2?.Validado == true)
{
await EnviarParaBackend(dadosEtapa1, dadosEtapa2);
}
}
Histórico de Navegação Customizado
Serviços adicionais podem manter uma pilha auxiliar para oferecer funcionalidade de "voltar" entre diálogos.
public class GerenteHistoricoModais
{
private readonly Stack<IDialogReference> _historico = new();
private readonly IDialogService _servico;
public async Task AddToHistory(string titulo, DialogParameters p)
{
var novaJanela = await _servico.ShowAsync<T>(titulo, p);
_historico.Push(novaJanela);
}
public async Task<bool> VoltarPagina()
{
if (_historico.Count < 2) return false; // Mínimo deve existir
_ = _historico.Pop(); // Remove o topo
_historico.Peek().Close(); // Fecha o antigo topo
return true;
}
}
Otimização e Solução de Problemas
Prevenção de Vazamento de Memória
Componentes modais que assinam eventos externos devem limpar essas subscrições ao serem destruídos ou fechados, implementando IDisposable.
public partial class DialogoConexo : IDisposable
{
private IDisposable? _assinaturaEvento;
protected override void OnInitialized()
{
_assinaturaEvento = ServicoGlobal.EventoUpdate.Subscribe(TratarAtualizacao);
DialogInstance.RegisterOnCloseHandler(LimparRecursos);
}
private void LimparRecursos()
{
_assinaturaEvento?.Dispose();
}
public void Dispose()
{
LimparRecursos();
}
}
Passo de Grandes Conjuntos de Dados
Nunca passe listas volumosas via parâmetros de diálogo. Utilize um serviço temporário compartilhado.
// Errado: Passando lista pesada diretamente
// var dialog = await Show<Editor>(new DialogParameters { ["Dados"] = listaGrande });
// Correto: Passando ID chave
_dataCache.TemporarioSet(dataId);
var dialog = await Show<Editor>(new DialogParameters { ["IdChave"] = dataId });
Tratamento de Eventos Residuais
Se um componente tentar interagir após o fechamento, verifique se a instância ainda está válida ou se o tratamento de eventos foi desvinculado no momento do descarte.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (IsDisposed) return; // Garante segurança
if (firstRender)
{
await CarregarEstadoInicial();
}
}