Arquitetura de Pilha de Diálogos e Manipulação de Retornos com MudBlazor

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();
    }
}

Tags: MudBlazor Blazor C# DependencyInjection UIComponents

Publicado em 10-21 22:13