MVVM com CommunityToolkit no .NET MAUI: Guia Prático com Source Generators

Aprenda a usar MVVM no .NET MAUI com CommunityToolkit.Mvvm. Guia prático com source generators, ObservableProperty, RelayCommand, Messenger, validação de dados e exemplos completos de código.

Por Que o MVVM é Essencial no .NET MAUI

Se você já trabalhou com .NET MAUI por algum tempo, sabe que colocar toda a lógica no code-behind das páginas XAML funciona — até que não funciona mais. A aplicação cresce, a manutenção vira um pesadelo e testar qualquer coisa se torna basicamente impossível. É aí que o padrão MVVM (Model-View-ViewModel) entra em cena.

O MVVM separa a aplicação em três camadas:

  • Model: Os dados e a lógica de negócio. Classes como Produto, Utilizador ou Pedido.
  • View: A interface gráfica — as páginas XAML que o utilizador vê e interage.
  • ViewModel: A ponte entre os dois. Contém a lógica de apresentação, expõe propriedades e comandos que a View consome via data binding.

A grande vantagem? Os ViewModels são classes C# puras, sem dependência do runtime MAUI. Você pode testá-los com xUnit ou NUnit num projeto de testes comum, sem emulador, sem dispositivo físico — apenas código executando em milissegundos. Honestamente, isso por si só já vale a mudança.

Mas escrever ViewModels no MVVM "clássico" exige muito código repetitivo. Implementar INotifyPropertyChanged, criar propriedades com getters e setters que chamam OnPropertyChanged, instanciar comandos manualmente... É tedioso. E é exatamente esse problema que o CommunityToolkit.Mvvm resolve.

O que é o CommunityToolkit.Mvvm

O CommunityToolkit.Mvvm (também conhecido como MVVM Toolkit) é uma biblioteca moderna, leve e modular mantida pela Microsoft como parte do .NET Community Toolkit. Ela fornece implementações de referência para os padrões mais comuns do MVVM, com foco especial em source generators — geradores de código que criam automaticamente todo o boilerplate durante a compilação.

Depois que comecei a usar no dia a dia, não consigo mais imaginar voltar atrás.

As principais características:

  • Independente de plataforma: Funciona com .NET MAUI, WPF, WinForms, Avalonia — qualquer framework que suporte MVVM.
  • Modular (à la carte): Use apenas o que precisa. Sem acoplamento forçado.
  • Source Generators: Os atributos [ObservableProperty] e [RelayCommand] eliminam centenas de linhas de código repetitivo.
  • Performática: A geração acontece em tempo de compilação, sem impacto no runtime.
  • Suporte a AOT: Totalmente compatível com compilação Ahead-of-Time, essencial para apps iOS e cenários de trimming.

Configuração Inicial do Projeto

Vamos à prática. Crie um projeto .NET MAUI (se ainda não tiver um):

dotnet new maui -n MinhaAppMvvm
cd MinhaAppMvvm

Agora instale o pacote NuGet:

dotnet add package CommunityToolkit.Mvvm

Na versão atual (9.x), o pacote é compatível com .NET 8, .NET 9 e .NET 10. A instalação é simples e não precisa de nenhuma configuração adicional no MauiProgram.cs — o toolkit funciona através de atributos e source generators, nada mais.

Estrutura de Pastas Recomendada

Uma organização que funciona bem:

MinhaAppMvvm/
├── Models/
│   └── TarefaItem.cs
├── ViewModels/
│   └── TarefasViewModel.cs
├── Views/
│   └── TarefasPage.xaml
├── Services/
│   └── ITarefaService.cs
│   └── TarefaService.cs
└── MauiProgram.cs

Não é obrigatória, mas é uma convenção bastante adotada que facilita a navegação no projeto.

ObservableProperty: Propriedades Reativas Sem Boilerplate

O atributo [ObservableProperty] é, sem dúvida, o recurso mais impactante do toolkit. Transforma um simples campo privado numa propriedade completa com notificação de mudanças — tudo gerado em tempo de compilação.

Antes: MVVM Clássico

Olha quanto código era necessário para uma única propriedade reativa:

public class TarefasViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _titulo = string.Empty;
    public string Titulo
    {
        get => _titulo;
        set
        {
            if (_titulo != value)
            {
                _titulo = value;
                PropertyChanged?.Invoke(this,
                    new PropertyChangedEventArgs(nameof(Titulo)));
            }
        }
    }

    private bool _estaConcluida;
    public bool EstaConcluida
    {
        get => _estaConcluida;
        set
        {
            if (_estaConcluida != value)
            {
                _estaConcluida = value;
                PropertyChanged?.Invoke(this,
                    new PropertyChangedEventArgs(nameof(EstaConcluida)));
            }
        }
    }
}

Mais de 30 linhas para duas propriedades. Agora imagine um ViewModel com 10...

Depois: Com CommunityToolkit.Mvvm

using CommunityToolkit.Mvvm.ComponentModel;

public partial class TarefasViewModel : ObservableObject
{
    [ObservableProperty]
    private string _titulo = string.Empty;

    [ObservableProperty]
    private bool _estaConcluida;
}

Apenas 8 linhas. O source generator cria automaticamente as propriedades Titulo e EstaConcluida (em PascalCase) com toda a lógica de notificação. Dois pontos importantes aqui:

  • A classe deve ser marcada como partial — isso permite que o source generator adicione código gerado noutra parte da classe.
  • A classe deve herdar de ObservableObject, que já implementa INotifyPropertyChanged e INotifyPropertyChanging.

NotifyPropertyChangedFor: Propriedades Dependentes

É comum uma propriedade depender de outra. Por exemplo, quando EstaConcluida muda, talvez você queira notificar a View que CorDoStatus também mudou:

public partial class TarefasViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(CorDoStatus))]
    private bool _estaConcluida;

    public string CorDoStatus => EstaConcluida ? "Green" : "Gray";
}

Quando EstaConcluida é alterada, o source generator garante que a notificação PropertyChanged seja disparada também para CorDoStatus. Na View, ambas serão atualizadas sem código adicional.

Partial Properties (v8.4.0+)

A partir da versão 8.4.0, é possível usar partial properties em vez de campos. Esta é a abordagem recomendada atualmente (especialmente para AOT e trimming):

public partial class TarefasViewModel : ObservableObject
{
    [ObservableProperty]
    public partial string Titulo { get; set; }

    [ObservableProperty]
    public partial bool EstaConcluida { get; set; }
}

O source generator cria o campo de apoio e a implementação completa. A vantagem é melhor compatibilidade com AOT e uma sintaxe mais limpa. Se você está começando um projeto novo, vá por esse caminho.

RelayCommand e AsyncRelayCommand: Comandos Simplificados

Comandos são o mecanismo principal para a View invocar ações no ViewModel. O atributo [RelayCommand] transforma qualquer método num comando bindável, sem instanciação manual.

Comando Síncrono

using CommunityToolkit.Mvvm.Input;

public partial class TarefasViewModel : ObservableObject
{
    [ObservableProperty]
    private string _novaTarefa = string.Empty;

    [ObservableProperty]
    private ObservableCollection<string> _tarefas = new();

    [RelayCommand]
    private void AdicionarTarefa()
    {
        if (!string.IsNullOrWhiteSpace(NovaTarefa))
        {
            Tarefas.Add(NovaTarefa);
            NovaTarefa = string.Empty;
        }
    }
}

O source generator cria uma propriedade AdicionarTarefaCommand do tipo RelayCommand. Na XAML, basta fazer o binding:

<Button Text="Adicionar"
        Command="{Binding AdicionarTarefaCommand}" />

A convenção é simples: o método AdicionarTarefa gera o comando AdicionarTarefaCommand.

Comando Assíncrono

Para operações com I/O (chamadas de API, acesso a banco de dados, etc.), use métodos assíncronos:

[RelayCommand]
private async Task CarregarTarefasAsync()
{
    var resultado = await _tarefaService.ObterTodasAsync();
    Tarefas = new ObservableCollection<string>(resultado);
}

O source generator detecta que o método retorna Task e cria um AsyncRelayCommand em vez de um RelayCommand. Detalhe importante: por padrão, o AsyncRelayCommand não permite execução concorrente. Se o utilizador clicar várias vezes no botão, só a primeira execução será processada. Isso evita problemas clássicos como requisições duplicadas.

E se o método terminar com Async (como CarregarTarefasAsync), o comando gerado omite o sufixo — fica CarregarTarefasCommand, não CarregarTarefasAsyncCommand.

CanExecute: Controlar Quando o Comando Está Disponível

Muitas vezes um botão só deve estar ativo em certas condições. O parâmetro CanExecute resolve isso de forma elegante:

public partial class TarefasViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(AdicionarTarefaCommand))]
    private string _novaTarefa = string.Empty;

    [RelayCommand(CanExecute = nameof(PodeAdicionarTarefa))]
    private void AdicionarTarefa()
    {
        Tarefas.Add(NovaTarefa);
        NovaTarefa = string.Empty;
    }

    private bool PodeAdicionarTarefa()
        => !string.IsNullOrWhiteSpace(NovaTarefa);
}

O atributo [NotifyCanExecuteChangedFor] garante que, sempre que NovaTarefa mudar, o comando reavalie se pode ser executado. Na prática, o botão fica desativado enquanto o campo estiver vazio, e é ativado quando o utilizador começa a digitar. Funciona perfeitamente.

Messenger: Comunicação Desacoplada entre Componentes

Em aplicações reais, os ViewModels precisam comunicar entre si. Por exemplo, quando uma tarefa é criada num ecrã e a lista noutro ecrã precisa atualizar. O MVVM Toolkit oferece o IMessenger para resolver este problema sem criar dependências diretas entre componentes.

Definir a Mensagem

using CommunityToolkit.Mvvm.Messaging.Messages;

public class TarefaCriadaMessage : ValueChangedMessage<string>
{
    public TarefaCriadaMessage(string titulo) : base(titulo) { }
}

Enviar a Mensagem

using CommunityToolkit.Mvvm.Messaging;

[RelayCommand]
private void AdicionarTarefa()
{
    if (!string.IsNullOrWhiteSpace(NovaTarefa))
    {
        Tarefas.Add(NovaTarefa);
        WeakReferenceMessenger.Default.Send(
            new TarefaCriadaMessage(NovaTarefa));
        NovaTarefa = string.Empty;
    }
}

Receber a Mensagem

public partial class ResumoViewModel : ObservableObject,
    IRecipient<TarefaCriadaMessage>
{
    [ObservableProperty]
    private int _totalTarefas;

    public ResumoViewModel()
    {
        WeakReferenceMessenger.Default.Register(this);
    }

    public void Receive(TarefaCriadaMessage message)
    {
        TotalTarefas++;
    }
}

O WeakReferenceMessenger usa referências fracas, então os destinatários podem ser coletados pelo garbage collector mesmo que ainda estejam registados. Isso previne vazamentos de memória — um problema que era bastante comum com o antigo MessagingCenter do Xamarin.Forms.

Validação de Dados com ObservableValidator

Para formulários que exigem validação, o toolkit tem o ObservableValidator — uma classe que combina ObservableObject com validação baseada em DataAnnotations. É surpreendentemente prático.

using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;

public partial class CriarContaViewModel : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required(ErrorMessage = "O nome é obrigatório")]
    [MinLength(3, ErrorMessage = "O nome deve ter pelo menos 3 caracteres")]
    private string _nome = string.Empty;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required(ErrorMessage = "O email é obrigatório")]
    [EmailAddress(ErrorMessage = "Formato de email inválido")]
    private string _email = string.Empty;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required(ErrorMessage = "A senha é obrigatória")]
    [MinLength(8, ErrorMessage = "A senha deve ter pelo menos 8 caracteres")]
    private string _senha = string.Empty;

    [RelayCommand]
    private void CriarConta()
    {
        ValidateAllProperties();

        if (HasErrors)
            return;

        // Prosseguir com a criação da conta
    }
}

O atributo [NotifyDataErrorInfo] faz com que a validação execute automaticamente sempre que a propriedade é alterada. Na XAML, dá para apresentar os erros de diferentes formas — alterando a cor da borda, exibindo uma mensagem abaixo do input, enfim, a escolha é sua.

Injeção de Dependências e Navegação com MVVM

O .NET MAUI tem suporte nativo a injeção de dependências via Microsoft.Extensions.DependencyInjection. A integração com MVVM é natural e, sinceramente, deixa tudo muito mais limpo.

Registar Serviços, ViewModels e Views

// MauiProgram.cs
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // Serviços
        builder.Services.AddSingleton<ITarefaService, TarefaService>();

        // ViewModels
        builder.Services.AddTransient<TarefasViewModel>();
        builder.Services.AddTransient<DetalhesTarefaViewModel>();

        // Views
        builder.Services.AddTransient<TarefasPage>();
        builder.Services.AddTransient<DetalhesTarefaPage>();

        // Rotas
        Routing.RegisterRoute(nameof(DetalhesTarefaPage),
            typeof(DetalhesTarefaPage));

        return builder.Build();
    }
}

Injetar Dependências no ViewModel

public partial class TarefasViewModel : ObservableObject
{
    private readonly ITarefaService _tarefaService;

    public TarefasViewModel(ITarefaService tarefaService)
    {
        _tarefaService = tarefaService;
    }

    [ObservableProperty]
    private ObservableCollection<TarefaItem> _tarefas = new();

    [RelayCommand]
    private async Task CarregarTarefasAsync()
    {
        var resultado = await _tarefaService.ObterTodasAsync();
        Tarefas = new ObservableCollection<TarefaItem>(resultado);
    }
}

Conectar a View ao ViewModel

No code-behind da página, receba o ViewModel por injeção de dependências e defina como BindingContext:

public partial class TarefasPage : ContentPage
{
    public TarefasPage(TarefasViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

Simples assim.

Navegação com Shell e Passagem de Parâmetros

Para navegar entre páginas passando dados, combine Shell Navigation com IQueryAttributable:

// No ViewModel de origem
[RelayCommand]
private async Task NavegarParaDetalhesAsync(TarefaItem tarefa)
{
    await Shell.Current.GoToAsync(nameof(DetalhesTarefaPage),
        new Dictionary<string, object>
        {
            { "Tarefa", tarefa }
        });
}

// No ViewModel de destino
public partial class DetalhesTarefaViewModel : ObservableObject,
    IQueryAttributable
{
    [ObservableProperty]
    private TarefaItem? _tarefa;

    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.TryGetValue("Tarefa", out var tarefa))
        {
            Tarefa = tarefa as TarefaItem;
        }
    }
}

A interface IQueryAttributable é a abordagem recomendada — segura para trimming e AOT, ao contrário do antigo [QueryProperty].

Exemplo Completo: App de Gestão de Tarefas

Agora vamos reunir tudo num exemplo funcional. Esta mini aplicação demonstra MVVM, injeção de dependências, comandos assíncronos e data binding — tudo junto.

Model

// Models/TarefaItem.cs
public class TarefaItem
{
    public int Id { get; set; }
    public string Titulo { get; set; } = string.Empty;
    public bool EstaConcluida { get; set; }
    public DateTime CriadaEm { get; set; } = DateTime.UtcNow;
}

Serviço

// Services/ITarefaService.cs
public interface ITarefaService
{
    Task<List<TarefaItem>> ObterTodasAsync();
    Task AdicionarAsync(TarefaItem tarefa);
    Task AlternarConclusaoAsync(int id);
    Task RemoverAsync(int id);
}

// Services/TarefaService.cs
public class TarefaService : ITarefaService
{
    private readonly List<TarefaItem> _tarefas = new();
    private int _proximoId = 1;

    public Task<List<TarefaItem>> ObterTodasAsync()
        => Task.FromResult(_tarefas.ToList());

    public Task AdicionarAsync(TarefaItem tarefa)
    {
        tarefa.Id = _proximoId++;
        _tarefas.Add(tarefa);
        return Task.CompletedTask;
    }

    public Task AlternarConclusaoAsync(int id)
    {
        var tarefa = _tarefas.FirstOrDefault(t => t.Id == id);
        if (tarefa is not null)
            tarefa.EstaConcluida = !tarefa.EstaConcluida;
        return Task.CompletedTask;
    }

    public Task RemoverAsync(int id)
    {
        _tarefas.RemoveAll(t => t.Id == id);
        return Task.CompletedTask;
    }
}

ViewModel

// ViewModels/TarefasViewModel.cs
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class TarefasViewModel : ObservableObject
{
    private readonly ITarefaService _tarefaService;

    public TarefasViewModel(ITarefaService tarefaService)
    {
        _tarefaService = tarefaService;
    }

    [ObservableProperty]
    private ObservableCollection<TarefaItem> _tarefas = new();

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(AdicionarTarefaCommand))]
    private string _novaTarefa = string.Empty;

    [ObservableProperty]
    private bool _estaCarregando;

    public int TotalPendentes =>
        Tarefas.Count(t => !t.EstaConcluida);

    [RelayCommand]
    private async Task CarregarTarefasAsync()
    {
        EstaCarregando = true;
        var resultado = await _tarefaService.ObterTodasAsync();
        Tarefas = new ObservableCollection<TarefaItem>(resultado);
        OnPropertyChanged(nameof(TotalPendentes));
        EstaCarregando = false;
    }

    [RelayCommand(CanExecute = nameof(PodeAdicionarTarefa))]
    private async Task AdicionarTarefaAsync()
    {
        var tarefa = new TarefaItem { Titulo = NovaTarefa };
        await _tarefaService.AdicionarAsync(tarefa);
        Tarefas.Add(tarefa);
        NovaTarefa = string.Empty;
        OnPropertyChanged(nameof(TotalPendentes));
    }

    private bool PodeAdicionarTarefa()
        => !string.IsNullOrWhiteSpace(NovaTarefa);

    [RelayCommand]
    private async Task RemoverTarefaAsync(TarefaItem tarefa)
    {
        await _tarefaService.RemoverAsync(tarefa.Id);
        Tarefas.Remove(tarefa);
        OnPropertyChanged(nameof(TotalPendentes));
    }
}

View (XAML)

<!-- Views/TarefasPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:MinhaAppMvvm.ViewModels"
             x:Class="MinhaAppMvvm.Views.TarefasPage"
             x:DataType="vm:TarefasViewModel"
             Title="As Minhas Tarefas">

    <Grid RowDefinitions="Auto,*,Auto" Padding="16">

        <!-- Barra de entrada -->
        <Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
            <Entry Placeholder="Nova tarefa..."
                   Text="{Binding NovaTarefa}" />
            <Button Grid.Column="1"
                    Text="+"
                    Command="{Binding AdicionarTarefaCommand}" />
        </Grid>

        <!-- Lista de tarefas -->
        <CollectionView Grid.Row="1"
                        ItemsSource="{Binding Tarefas}"
                        EmptyView="Nenhuma tarefa encontrada">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="models:TarefaItem">
                    <SwipeView>
                        <SwipeView.RightItems>
                            <SwipeItems>
                                <SwipeItem Text="Apagar"
                                    BackgroundColor="Red"
                                    Command="{Binding
                                        Source={RelativeSource
                                            AncestorType={x:Type vm:TarefasViewModel}},
                                        Path=RemoverTarefaCommand}"
                                    CommandParameter="{Binding .}" />
                            </SwipeItems>
                        </SwipeView.RightItems>
                        <Grid Padding="8" ColumnDefinitions="Auto,*">
                            <CheckBox IsChecked="{Binding EstaConcluida}" />
                            <Label Grid.Column="1"
                                   Text="{Binding Titulo}"
                                   VerticalOptions="Center" />
                        </Grid>
                    </SwipeView>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>

        <!-- Resumo -->
        <Label Grid.Row="2"
               Text="{Binding TotalPendentes,
                   StringFormat='{0} tarefas pendentes'}"
               HorizontalOptions="Center"
               Margin="0,8,0,0" />

    </Grid>

</ContentPage>

Repare no uso de x:DataType em toda a XAML. Isso ativa os compiled bindings — bindings verificados em tempo de compilação que são bem mais rápidos que os baseados em reflexão. É uma das otimizações de performance mais importantes num projeto .NET MAUI (e muita gente esquece de ativar).

Boas Práticas e Armadilhas Comuns

Ao longo do tempo trabalhando com o toolkit e acompanhando a comunidade, alguns padrões ficam claros. Aqui vai o que funciona e o que dá problema.

Faça

  • Use sempre partial nas classes: Sem o modificador partial, o source generator não consegue adicionar código e você verá erros de compilação. É o erro mais comum para quem está começando.
  • Prefira ObservableCollection<T> para listas: Uma List<T> comum não notifica a UI quando itens são adicionados ou removidos.
  • Ative compiled bindings com x:DataType: Melhora a performance e captura erros de binding em tempo de compilação.
  • Registe ViewModels como Transient: Na maioria dos casos, cada página deve receber uma nova instância. Use Singleton apenas quando precisar de estado partilhado.
  • Use IQueryAttributable para parâmetros de navegação: É seguro para trimming e AOT.

Evite

  • Não acesse a UI a partir do ViewModel: O ViewModel nunca deve referenciar controles XAML. Toda comunicação é via data binding e comandos.
  • Não use async void em comandos: Use sempre async Task. O AsyncRelayCommand trata exceções corretamente; async void pode causar crashes silenciosos.
  • Não crie ViewModels com new: Use injeção de dependências. Criar ViewModels manualmente impede a injeção de serviços e dificulta os testes.
  • Não esqueça de cancelar subscrições do Messenger: Se o ViewModel implementa IRecipient<T>, faça UnregisterAll quando a página for descartada para evitar vazamentos de memória.

Como Visualizar o Código Gerado

Se tiver curiosidade sobre o que o source generator produz (recomendo dar uma olhada — é uma boa forma de aprender), pode inspecionar no Visual Studio:

  1. Expanda o projeto no Solution Explorer.
  2. Navegue até Dependencies → Analyzers → CommunityToolkit.Mvvm.SourceGenerators.
  3. Encontre os ficheiros gerados para cada classe anotada.

Isso ajuda a entender exatamente o que acontece "por trás dos panos" e é útil para depuração quando algo não funciona como esperado.

Perguntas Frequentes

Qual a diferença entre ObservableObject e ObservableValidator?

ObservableObject é a classe base para ViewModels que precisam de notificação de mudanças. ObservableValidator herda de ObservableObject e adiciona validação com DataAnnotations. Use ObservableValidator em formulários — ecrãs de registo, login, edição de perfil. Para o resto, ObservableObject basta.

Posso usar o CommunityToolkit.Mvvm sem .NET MAUI?

Com certeza. O toolkit é independente de plataforma. Funciona com WPF, WinForms, Avalonia, UNO Platform, ou até em apps de consola. A API é a mesma em todo lado, o que é ótimo para bibliotecas partilhadas entre projetos.

O source generator afeta a performance em tempo de execução?

Não. Todo o código é gerado na compilação. O resultado é idêntico ao que você escreveria manualmente — sem reflexão, sem overhead de runtime. Na verdade, os source generators frequentemente geram código mais otimizado do que o que a maioria dos desenvolvedores escreveria, especialmente no que toca a alocações e invocação de eventos.

WeakReferenceMessenger ou StrongReferenceMessenger?

Para apps móveis, vá de WeakReferenceMessenger. Ele usa referências fracas, então os destinatários podem ser coletados pelo garbage collector automaticamente. Isso previne vazamentos de memória — algo crítico em dispositivos com recursos limitados. O StrongReferenceMessenger é mais rápido, mas exige que você faça unregister manualmente. Use apenas quando tiver controle total sobre o ciclo de vida dos objetos.

Como faço para testar unitariamente um ViewModel?

Os ViewModels com o toolkit são classes C# puras — não dependem do runtime MAUI. Crie um projeto de testes xUnit ou NUnit, instancie o ViewModel (com mocks dos serviços) e teste propriedades e comandos diretamente. Para verificar notificações, subscreva PropertyChanged e valide que é disparado quando esperado. Sem complicação.

Sobre o Autor Editorial Team

Our team of expert writers and editors.