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
- Microsoft Learn — Desktop Guide (Windows Forms .NET) e Desktop Guide (WPF .NET): https://learn.microsoft.com/dotnet/desktop/
- Microsoft Learn — What is .NET MAUI?: https://learn.microsoft.com/dotnet/maui/what-is-maui
- Avalonia — documentação oficial: https://docs.avaloniaui.net/
- Microsoft Learn — Introduction to the MVVM Toolkit: https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/
- John Gossman — Introduction to Model/View/ViewModel pattern for building WPF apps (blog, 2005): https://learn.microsoft.com/archive/blogs/johngossman/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps
- Microsoft Learn — ASP.NET Core Blazor Hybrid: https://learn.microsoft.com/aspnet/core/blazor/hybrid/
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).