Interfaces no .NET: WinForms, WPF, MAUI, Avalonia e o que escolher

Interfaces no .NET: WinForms, WPF, MAUI, Avalonia e o que escolher

Do Windows Forms de 2002 ao .NET MAUI e ao Avalonia: a história das interfaces gráficas no .NET, controles nativos contra desenho próprio, o mesmo contador em quatro tecnologias, o padrão MVVM com o CommunityToolkit e um critério para escolher a tecnologia de cada interface da Livraria.
Linguagem C#

• • 16 min de leitura

Nem toda interface é uma página web. O caixa da loja física, o aplicativo no celular do cliente e a ferramenta interna do estoque também são interfaces, e o .NET acumulou, em vinte e cinco anos, uma família inteira de tecnologias para construí-las. Este artigo percorre essa história, explica os dois modelos de desenho de tela que as separam e introduz o padrão MVVM, base dos próximos dois artigos.

Vinte e cinco anos em uma tabela

Ano Tecnologia Plataformas Como desenha a tela Situação em 2026
2002 Windows Forms Windows controles nativos do Win32 via GDI/GDI+ suportado, código aberto, ativo em manutenção
2006 WPF Windows desenho próprio via DirectX; XAML suportado, código aberto, evolução lenta
2007 Silverlight navegador (plugin) desenho próprio; XAML encerrado em 2021
2011 Xamarin iOS, Android controles nativos de cada plataforma suporte encerrado em maio de 2024
2012–2015 WinRT / UWP Windows 8/10, Xbox, HoloLens desenho próprio; XAML em manutenção; sucedido pelo WinUI 3
2016 Avalonia Windows, macOS, Linux, iOS, Android, navegador desenho próprio via Skia; XAML ativo, código aberto (MIT)
2018 Uno Platform as mesmas API do WinUI em todas as plataformas ativo, código aberto
2021 WinUI 3 / Windows App SDK Windows desenho próprio; XAML ativo
2022 .NET MAUI iOS, Android, Windows, macOS controles nativos via handlers; XAML ativo
2022 Blazor Hybrid onde houver um WebView HTML e CSS num WebView, com C# nativo ativo

Três momentos explicam o mapa atual.

Em 2018, a Microsoft abriu o código do Windows Forms e do WPF e os levou ao .NET Core 3.0 (2019). Eles saíram do .NET Framework, congelado, e passaram a ganhar desempenho, suporte a telas de alta densidade e as versões novas de C#. Continuam só para Windows, porque são construídos sobre APIs do Windows.

Em 2016, a Microsoft comprou a Xamarin, empresa fundada em 2011 por Miguel de Icaza e Nat Friedman, criadores do Mono, a implementação de .NET que tornou o C# multiplataforma antes de a própria Microsoft fazê-lo, como contamos em "Da Rivalidade com o Java ao Código Aberto". O Xamarin.Forms evoluiu para o .NET MAUI (Multi-platform App UI), lançado com o .NET 6 em 2022.

Fora da Microsoft, o Avalonia começou em 2013 como um projeto pessoal de Steven Kirk chamado Perspex: um "WPF que rodasse em qualquer lugar". Renomeado em 2016, cresceu até ser usado em ferramentas como o JetBrains Rider (partes da interface) e aplicações industriais e financeiras que precisam de Linux. É mantido por uma empresa própria com um núcleo aberto sob licença MIT.

Controles nativos ou desenho próprio

A diferença técnica mais importante entre essas tecnologias é quem desenha os pixels.

Controles nativos (WinForms, MAUI). O framework cria o botão do sistema operacional: um BUTTON do Win32, um UIButton do iOS, um android.widget.Button. O C# só configura e escuta. A vantagem é a aparência e o comportamento exatamente iguais aos dos outros aplicativos da plataforma, com acessibilidade e integração com o sistema, como leitores de tela, texto dinâmico e seleção de texto, sem esforço. A desvantagem é que cada plataforma tem seus próprios limites e diferenças. Um mesmo layout pode ficar ligeiramente diferente no iPhone e no Android, e personalizações profundas exigem código específico por plataforma.

Desenho próprio (WPF, Avalonia, WinUI, Uno em alguns modos, e fora do .NET, o Flutter). O framework recebe uma janela vazia do sistema e desenha tudo sozinho: cada borda, cada texto, cada sombra, usando DirectX, Skia ou outra biblioteca gráfica. A vantagem é que os pixels são idênticos em todas as plataformas, a personalização é total (qualquer controle pode ser redesenhado por completo com templates) e as animações são ricas. A desvantagem é que o framework precisa reimplementar o comportamento esperado de cada controle e integrar a acessibilidade com cada sistema, e o resultado pode não parecer "nativo".

Nenhum modelo é superior. A escolha depende de quanto o produto precisa parecer da plataforma e quanto precisa parecer igual em todas.

Quatro sabores do mesmo contador

Para sentir as diferenças, vejamos um contador de exemplares no caixa em cada tecnologia.

Windows Forms: controles criados em código (ou pelo designer visual do Visual Studio, que gera esse código), com eventos ligados a métodos:

public sealed class JanelaDoCaixa : Form
{
    private int quantidade = 1;
    private readonly Label rotulo = new() { Text = "1", AutoSize = true, Location = new(20, 20) };

    public JanelaDoCaixa()
    {
        Text = "Caixa";
        var mais = new Button { Text = "+", Location = new(60, 16) };
        mais.Click += (_, _) => { quantidade++; rotulo.Text = quantidade.ToString(); };
        Controls.AddRange([rotulo, mais]);
    }
}

É direto e produtivo para ferramentas internas simples. O ponto fraco aparece com o crescimento: a lógica mora nos tratadores de evento da janela, o estado é copiado à mão para os controles (rotulo.Text = ...), e testar a lógica exige instanciar a janela.

WPF: a tela é descrita em XAML, uma linguagem de marcação baseada em XML, e ligada aos dados por binding:

<StackPanel Orientation="Horizontal">
    <TextBlock Text="{Binding Quantidade}" />
    <Button Content="+" Command="{Binding AumentarCommand}" />
</StackPanel>

.NET MAUI e Avalonia usam dialetos de XAML muito parecidos com o do WPF:

<!-- MAUI -->
<HorizontalStackLayout>
    <Label Text="{Binding Quantidade}" />
    <Button Text="+" Command="{Binding AumentarCommand}" />
</HorizontalStackLayout>

<!-- Avalonia -->
<StackPanel Orientation="Horizontal">
    <TextBlock Text="{Binding Quantidade}" />
    <Button Content="+" Command="{Binding AumentarCommand}" />
</StackPanel>

Os três XAML não contêm lógica nenhuma. Eles mostram uma propriedade Quantidade e ligam o botão a um comando AumentarCommand, que vivem em outra classe. Essa separação tem nome.

MVVM: a tela como função do estado

O Model-View-ViewModel foi descrito em 2005 por John Gossman, arquiteto do WPF e do Expression Blend, como a forma natural de usar o binding que o WPF trazia. Ele separa três papéis:

  • Model: o domínio e os serviços, isto é, o que a Livraria já tem: ProdutoResposta, clientes da API, regras.
  • View: a tela em XAML, que só declara como mostrar e a que se ligar.
  • ViewModel: uma classe C# comum que expõe o estado da tela (propriedades) e as ações possíveis (comandos). Não conhece a View, nem botões, nem cores.

O binding liga os dois lados. Quando uma propriedade do ViewModel muda, a View se atualiza. Quando o usuário edita um campo, a propriedade do ViewModel recebe o valor. Quem já leu os artigos de Blazor reconhece a ideia: a interface é uma função do estado.

O mecanismo por baixo são duas interfaces da BCL. INotifyPropertyChanged avisa a View de que uma propriedade mudou:

public sealed class CaixaViewModel : INotifyPropertyChanged
{
    private int quantidade = 1;

    public int Quantidade
    {
        get => quantidade;
        set
        {
            if (quantidade == value) return;
            quantidade = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Quantidade)));
        }
    }

    public ICommand AumentarCommand { get; }

    public CaixaViewModel() =>
        AumentarCommand = new ComandoSimples(() => Quantidade++, () => Quantidade < 20);

    public event PropertyChangedEventHandler? PropertyChanged;
}

E ICommand representa uma ação com Execute e CanExecute. Quando CanExecute devolve falso, o botão ligado ao comando fica desabilitado automaticamente. A regra "no máximo 20 exemplares" mora no ViewModel, e a View a respeita sem saber que ela existe.

Esse código é repetitivo, e por isso quase ninguém o escreve à mão hoje. O CommunityToolkit.Mvvm, biblioteca da Microsoft e da comunidade .NET, usa source generators, os de "Source generators: código que escreve código", para gerar tudo isso:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public sealed partial class CaixaViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(AumentarCommand))]
    private int quantidade = 1;

    [RelayCommand(CanExecute = nameof(PodeAumentar))]
    private void Aumentar() => Quantidade++;

    private bool PodeAumentar() => Quantidade < 20;
}

O gerador cria a propriedade Quantidade com a notificação, o comando AumentarCommand e a reavaliação do CanExecute sempre que Quantidade muda. As versões recentes, com C# 14, também aceitam o atributo em propriedades parciais, e não só em campos. A classe precisa ser partial para o gerador completá-la.

A consequência mais valiosa do MVVM é a testabilidade. O ViewModel é uma classe C# sem dependência de interface gráfica, e pode ser testado com xUnit como qualquer outra:

[Fact]
public void Nao_permite_mais_de_20_exemplares()
{
    var vm = new CaixaViewModel { Quantidade = 20 };
    Assert.False(vm.AumentarCommand.CanExecute(null));
}

Toda a lógica de tela, como validações, estados de habilitação, carregamento e mensagens de erro, é testada sem abrir uma janela. As Views ficam finas, e o que é difícil de testar automaticamente fica também sem lógica para testar.

O mesmo ViewModel funciona, com poucas ou nenhuma mudança, em WPF, Avalonia, MAUI, WinUI e Uno, que usam o mesmo modelo de binding. É por isso que o CommunityToolkit.Mvvm não depende de nenhum desses frameworks.

Blazor Hybrid: a web dentro do aplicativo

Uma alternativa que atravessa todas as outras é o Blazor Hybrid. O BlazorWebView é um controle disponível em MAUI, WPF e WinForms que hospeda componentes Blazor dentro de um aplicativo nativo. Os componentes rodam em .NET nativo, no processo do aplicativo, sem WebAssembly e sem servidor, e a tela é desenhada por um WebView do sistema com HTML e CSS. Isso permite:

  • Reaproveitar os componentes do site, como o carrinho de "Blazor: componentes, parâmetros, eventos e ciclo de vida", num aplicativo instalado.
  • Ter acesso total às APIs do dispositivo (arquivos, câmera, notificações) a partir do C#, sem as restrições do navegador.
  • Migrar aos poucos um aplicativo WinForms ou WPF antigo, substituindo telas uma a uma por componentes web.

O custo é a aparência de página web, e não de aplicativo nativo, além de um WebView a mais na memória.

Como escolher

Situação Escolha provável Por quê
Ferramenta interna simples, só Windows, equipe pequena Windows Forms produtividade altíssima com o designer; décadas de conhecimento acumulado
Aplicação corporativa rica, só Windows, já existente WPF binding e MVVM maduros, ecossistema enorme de controles
Aplicação Windows nova, com aparência do Windows 11 WinUI 3 os controles e o estilo atuais do Windows
Desktop em Windows, macOS e Linux Avalonia ou Uno desenho próprio, aparência idêntica, Linux com suporte de primeira classe
Celular (iOS e Android), com aparência nativa .NET MAUI controles nativos, acesso às APIs do dispositivo, um só projeto
Reaproveitar componentes do site num aplicativo Blazor Hybrid os mesmos componentes Razor, no desktop ou no celular

Duas observações evitam decisões erradas. O MAUI não tem suporte oficial a Linux. Para um aplicativo que precise rodar em Linux, Avalonia ou Uno são as opções. E o Windows Forms não é tecnologia morta: continua recebendo melhorias (suporte a modo escuro chegou como recurso experimental no .NET 9), é a forma mais rápida de fazer uma ferramenta interna funcional, e há milhões de linhas dele em produção. O erro é usá-lo para algo que vai crescer muito ou precisa rodar fora do Windows.

As interfaces da Livraria

Aplicando o critério à Livraria:

  • O caixa da loja física (ponto de venda) precisa rodar nos computadores do balcão. Parte deles é Windows, e as máquinas mais novas, mais baratas, vêm com Linux. Precisa funcionar com leitor de código de barras, aguentar o dia inteiro aberto e continuar vendendo se a internet cair por alguns minutos. É um caso de Avalonia com MVVM, e é o assunto do próximo artigo.
  • O aplicativo do cliente mostra o catálogo, o carrinho e o acompanhamento de pedidos, com notificações no celular. Precisa parecer nativo no iPhone e no Android e usar recursos do aparelho. É um caso de .NET MAUI, no artigo seguinte.

Os dois consomem a mesma API da Fase 6, com os mesmos contratos, a mesma autenticação por OpenID Connect e o mesmo hub de tempo real. É a demonstração prática de que o trabalho de separar contrato, domínio e interface, feito desde a Fase 5, rende em cada interface nova.

O .NET não tem uma única resposta para interfaces gráficas, e isso é uma consequência de vinte e cinco anos de plataformas que mudaram. O que atravessa todas as tecnologias atuais, com exceção do Windows Forms, é o mesmo modelo: telas declaradas em XAML ou em componentes, ligadas por binding a classes C# que guardam o estado e expõem ações, testáveis sem interface nenhuma.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Uma empresa tem um sistema de estoque em WinForms com 200 telas, usado por 40 pessoas em Windows. A diretoria quer "modernizar". Compare três caminhos: reescrever em WPF, reescrever em Avalonia e migrar gradualmente com Blazor Hybrid. Que perguntas decidem a escolha?

Ver resposta

✓ Resposta: Reescrever em WPF mantém tudo em Windows e ganha binding e MVVM, mas é uma reescrita completa de 200 telas sem ganho de plataforma. Só faz sentido se a interface precisa de recursos que o WinForms não oferece bem, como estilização rica ou layouts complexos. Avalonia também é reescrita completa, e o ganho é rodar em Linux e macOS. Só se justifica se essas plataformas forem um requisito real. Blazor Hybrid permite trocar tela por tela dentro do mesmo executável WinForms com BlazorWebView, entregando valor continuamente e reaproveitando as telas novas num eventual site. O custo é conviver com duas tecnologias por um tempo. As perguntas que decidem: (1) qual problema concreto a "modernização" resolve (aparência, manutenção, plataformas, contratação)? (2) as regras de negócio estão separadas das telas, ou espalhadas nos tratadores de evento? Se estão espalhadas, o primeiro passo em qualquer caminho é extraí-las; (3) há necessidade real fora do Windows? (4) a equipe tem experiência com web? Em muitos casos a resposta honesta é: extrair a lógica para serviços testáveis, manter o WinForms e migrar só as telas que mais incomodam.

Exercício 2

Explique por que um mesmo aplicativo MAUI pode ter um DatePicker com aparência bem diferente no iPhone e no Android, enquanto em Avalonia ele é igual nos dois. Dê uma situação em que cada comportamento é o desejável.

Ver resposta

✓ Resposta: O MAUI mapeia o DatePicker para o controle nativo de cada plataforma: o seletor de rolagem ou calendário do UIKit no iOS e o diálogo de calendário do Material no Android. Cada sistema desenha o seu. O Avalonia desenha o próprio controle com Skia, a partir do mesmo template, em qualquer plataforma. O comportamento nativo é o desejável num aplicativo de consumo, como o da Livraria para clientes: o usuário de iPhone espera o seletor do iPhone, com a mesma acessibilidade e os mesmos gestos que usa em todo o resto do aparelho. O comportamento idêntico é desejável quando o treinamento e a documentação precisam valer para todos, como no caixa da loja com máquinas Windows e Linux: um manual com capturas de tela serve para todas, e o suporte não precisa saber em que sistema o operador está.

Exercício 3

Reescreva o CaixaViewModel com o CommunityToolkit acrescentando uma propriedade Isbn (string) e um comando AdicionarCommand que só pode ser executado quando o ISBN tem 13 dígitos. Que atributo faz o botão habilitar e desabilitar enquanto o operador digita?

Ver resposta

✓ Resposta: [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AdicionarCommand))] private string isbn = "";, [RelayCommand(CanExecute = nameof(PodeAdicionar))] private void Adicionar() { /* adiciona o item */ Isbn = ""; } e private bool PodeAdicionar() => Isbn.Count(char.IsAsciiDigit) == 13;. O atributo que faz o botão reagir enquanto o operador digita é [NotifyCanExecuteChangedFor(nameof(AdicionarCommand))] na propriedade: a cada mudança de Isbn, o gerador dispara AdicionarCommand.NotifyCanExecuteChanged(), e a View reavalia CanExecute e habilita ou desabilita o botão. Sem ele, o botão ficaria preso no estado calculado na primeira avaliação. Na View, o campo usa binding bidirecional, que atualiza a propriedade a cada tecla. A validação completa, com o dígito verificador do [Isbn] de "Validação, ProblemDetails e OpenAPI", pode ser reaproveitada extraindo o cálculo para um método estático usado pelos dois.

Exercício 4

Por que o MVVM torna a lógica de tela testável e o padrão típico de Windows Forms não? Escreva o teste equivalente ao do artigo para a versão WinForms do contador e aponte as dificuldades.

Ver resposta

✓ Resposta: No WinForms típico, a lógica ("não passar de 20") fica no tratador Click, que é um método privado da janela, e o estado fica no texto do Label. Para testar, seria preciso: criar a Form (o que exige uma thread STA e, em alguns casos, um ambiente com interface gráfica, complicado em servidores de CI Linux), encontrar o botão entre os controles, simular o clique com PerformClick() vinte vezes e ler rotulo.Text como string. O teste fica frágil (quebra se o layout ou o texto mudar), lento e dependente do Windows. No MVVM, a mesma regra está numa propriedade e num CanExecute de uma classe comum: o teste instancia o ViewModel, define Quantidade = 20 e consulta CanExecute, em microssegundos, em qualquer sistema. A diferença não é de ferramenta, é de onde a lógica mora. O WinForms também pode usar binding e separar a lógica numa classe (o .NET 7 acrescentou suporte a comandos e binding no estilo MVVM ao WinForms), mas o hábito do designer leva ao código no tratador de evento.

Exercício 5

A Livraria quer que o carrinho do aplicativo móvel tenha exatamente o mesmo comportamento do carrinho do site. Compare duas estratégias: reaproveitar os componentes Blazor via Blazor Hybrid dentro do MAUI, ou reescrever a tela em XAML com um ViewModel. O que cada uma compartilha de verdade?

Ver resposta

✓ Resposta: Blazor Hybrid compartilha a camada de interface inteira: os mesmos componentes .razor, o mesmo CSS e o mesmo comportamento, com uma única implementação a manter. O ICarrinhos ganha uma terceira implementação (chamando a API direto do aplicativo, com o token do usuário), e o resto é igual. O custo é a aparência de página web dentro do aplicativo e a perda de controles nativos, como listas com os gestos de arrastar para apagar de cada sistema. XAML com ViewModel compartilha só o que está abaixo da interface: os contratos (Livraria.Contratos), talvez os serviços de cliente da API e as regras extraídas para classes comuns. A tela é outra, nativa, com comportamento próprio de cada plataforma. Na prática, "o mesmo comportamento" vem das regras: limites de quantidade, cálculo de subtotal e tratamento de conflitos de estoque. Se essas regras vivem na API e num pacote de cliente compartilhado, e não nos componentes, as duas estratégias produzem o mesmo comportamento, e a escolha fica entre velocidade de entrega (Blazor Hybrid) e experiência nativa (XAML).

Comentários

Mais em Linguagem C#

EF Core: Relacionamentos e Migrations
EF Core: Relacionamentos e Migrations

Autores e pedidos como entidades relacionadas no EF Core: um para muitos com…

Sequências Sob Demanda: IEnumerable, yield e Coleções Imutáveis
Sequências Sob Demanda: IEnumerable, yield e Coleções Imutáveis

O que o foreach faz por baixo com IEnumerable e IEnumerator, iteradores com…

Da Rivalidade com o Java ao Código Aberto: a História do C#
Da Rivalidade com o Java ao Código Aberto: a História do C#

A história do C#: Anders Hejlsberg, do Turbo Pascal ao Delphi, o processo da…