name: dotnet-wpf-mvvm description: WinForms→WPF MVVM migration plus new WPF screens — CommunityToolkit.Mvvm, WPF-UI, ViewModels, data binding, Commands, navigation, DI via Microsoft.Extensions.Hosting. Setup and E2E live in sibling skills. Triggers — MVVM, WinForms to WPF, CommunityToolkit, data binding, RelayCommand.
dotnet-wpf-mvvm
Skill para migrar projetos WinForms para WPF com MVVM e para construir novas telas WPF seguindo o padrao MVVM moderno com CommunityToolkit.Mvvm + WPF-UI.
Usa progressive disclosure — este arquivo contem o workflow e decisoes. Templates,
exemplos de codigo e guias detalhados ficam em references/ e sao lidos sob demanda.
Stack Recomendada
| Componente | Pacote NuGet | Funcao |
|---|---|---|
| MVVM Framework | CommunityToolkit.Mvvm |
ObservableObject, source generators |
| DI + Lifecycle | Microsoft.Extensions.Hosting |
IHost, IServiceProvider |
| UI Framework | WPF-UI (Wpf.Ui) |
Fluent Design, NavigationView, Theming |
| Navegacao | Wpf.Ui.INavigationService | MVVM-friendly page navigation |
| Dialogs | Wpf.Ui.IContentDialogService | Substitui MessageBox |
Quando usar
- Migrar um Form WinForms para WPF com MVVM
- Adicionar MVVM a projeto WPF que usa code-behind
- Criar nova tela/pagina WPF com ViewModel
- Configurar navegacao entre paginas com DI
- Substituir event handlers por Commands
- Configurar DI em App.xaml.cs
Pre-requisitos
Antes de aplicar MVVM, o projeto deve ter:
- Services desacoplados — logica de negocio em classes
*Service.cs, nao em Forms/code-behind - Sem MessageBox em services — services retornam
Result<T>ou lancam excecoes - Target framework .NET 8+ — source generators exigem .NET moderno
Se o projeto nao atende esses requisitos, use a skill dotnet-desktop-setup primeiro para
desacoplar e configurar. O MVVM funciona melhor quando os services ja existem — o ViewModel
simplesmente orquestra chamadas aos services e expoe dados para a View.
Workflow: 6 Passos
Execute os passos em ordem. Cada passo verifica o estado atual antes de agir.
Passo 1: Diagnostico do Estado Atual
Avalie o projeto para entender o ponto de partida:
# Verificar framework UI
grep -r "UseWPF\|UseWindowsForms" *.csproj
# Verificar se CommunityToolkit.Mvvm ja esta instalado
grep -r "CommunityToolkit.Mvvm" *.csproj
# Contar event handlers no code-behind (quanto trabalho tem pela frente)
grep -rn "_Click\|_Changed\|_Loaded\|_SelectionChanged" *.xaml.cs *.cs
# Verificar services existentes
find . -name "*Service.cs" -type f
# Verificar se tem MessageBox em services (anti-padrao)
grep -rn "MessageBox" --include="*Service.cs"
Apresente o relatorio ao usuario:
- "Projeto X: WPF com WPF-UI, sem MVVM. 5 event handlers para migrar. 2 services existentes."
- "Pre-requisitos: OK" ou "Pre-requisitos: MessageBox encontrado em LicenseService.cs — desacoplar primeiro"
Passo 2: Instalar Pacotes
Adicione os pacotes necessarios via dotnet add:
dotnet add <projeto>.csproj package CommunityToolkit.Mvvm
dotnet add <projeto>.csproj package Microsoft.Extensions.Hosting
Se WPF-UI nao estiver instalado:
dotnet add <projeto>.csproj package WPF-UI
Verifique que o .csproj tem:
<UseWPF>true</UseWPF>
Passo 3: Configurar App.xaml.cs como Composition Root
Leia references/wpfui-integration.md para o template completo de App.xaml.cs.
O App.xaml.cs deve:
- Criar
IHostcomHost.CreateDefaultBuilder() - Registrar todos os services no DI container
- Registrar todos os ViewModels (Singleton para apps com NavigationView — ver Detalhe #27)
- Registrar todas as Pages/Windows (Transient ou Singleton conforme necessidade)
- Registrar services WPF-UI: INavigationService, IContentDialogService, IThemeService
- Iniciar o host em
OnStartup, parar emOnExit
Padrao de registro:
// Services de negocio
services.AddSingleton<ILicenseService, LicenseService>();
// ViewModels — Singleton para evitar memory leak quando assinam PropertyChanged
// de servicos Singleton (ver Detalhe #27)
services.AddSingleton<MainWindowViewModel>();
// Windows/Pages
services.AddTransient<MainWindow>();
Para apps simples (1 janela, sem navegacao entre paginas), o registro minimo e:
- MainWindow + MainWindowViewModel
- Services de negocio
- Nao precisa de INavigationService/IPageService
Passo 4: Criar ViewModels
Leia references/communitytoolkit-patterns.md para patterns detalhados.
Para cada tela, crie um ViewModel seguindo este template:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MeuProjeto.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
private readonly IMyService _service;
// Propriedades observaveis — o source generator cria a propriedade publica
[ObservableProperty]
private string _titulo;
[ObservableProperty]
private bool _isProcessando;
// Injecao de dependencia via construtor
public MainWindowViewModel(IMyService service)
{
_service = service;
}
// Commands — o source generator cria TituloCommand (IRelayCommand)
[RelayCommand]
private async Task CarregarDadosAsync()
{
IsProcessando = true;
try
{
var dados = await _service.ObterDadosAsync();
Titulo = dados.Nome;
}
finally
{
IsProcessando = false;
}
}
}
Regras criticas:
- A classe DEVE ser
partial— source generators precisam disso - Campos
[ObservableProperty]DEVEM serprivate—_namegera propriedadeName - Metodos
[RelayCommand]geram propriedade com sufixoCommand—Salvar()geraSalvarCommand - Metodos async geram
IAsyncRelayCommandcom cancelamento automatico
Passo 5: Refatorar Views (XAML)
Substitua event handlers por bindings e commands:
Antes (code-behind):
<Button Content="Carregar" Click="BtnCarregar_Click" />
<TextBox x:Name="txtNome" />
private void BtnCarregar_Click(object sender, RoutedEventArgs e)
{
txtNome.Text = _service.Carregar();
}
Depois (MVVM):
<Button Content="Carregar" Command="{Binding CarregarDadosCommand}" />
<TextBox Text="{Binding Titulo, UpdateSourceTrigger=PropertyChanged}" />
// Code-behind fica so com DI wiring
public MainWindow(MainWindowViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
Mapeamento rapido de controles:
| WinForms / Code-behind | WPF MVVM |
|---|---|
button.Click += handler |
Command="{Binding XCommand}" |
textBox.Text = valor |
Text="{Binding Propriedade}" |
listBox.Items.Add(x) |
ItemsSource="{Binding Lista}" + ObservableCollection<T> |
checkBox.Checked += handler |
IsChecked="{Binding Flag}" |
comboBox.SelectedItem |
SelectedItem="{Binding ItemSelecionado}" |
label.Content = texto |
Content="{Binding Texto}" |
progressBar.Value |
Value="{Binding Progresso}" |
control.Enabled = false |
IsEnabled="{Binding PodeExecutar}" ou CanExecute no Command |
element.Visibility = Visible/Collapsed |
Visibility="{Binding IsXxx, Converter={StaticResource BoolToVis}}" |
comboBox.SelectedItem (ComboBoxItem) |
SelectionChanged handler ou SelectedValue="{Binding Prop}" com SelectedValuePath |
scrollViewer.ScrollToTop() |
PropertyChanged handler no code-behind (excecao MVVM documentada) |
Mouse.OverrideCursor = Wait |
[ObservableProperty] bool IsLoading + trigger ou converter no XAML |
Dialogs MVVM-friendly:
// Em vez de: MessageBox.Show("Erro")
// Use Microsoft.Win32 para file dialogs:
var dialog = new Microsoft.Win32.OpenFileDialog { Filter = "HID files|*.hid" };
if (dialog.ShowDialog() == true)
{
CaminhoArquivo = dialog.FileName;
}
Para dialogs mais complexos, use IContentDialogService do WPF-UI
(veja references/wpfui-integration.md).
Passo 6: Verificacao
Apos aplicar MVVM, verifique:
- Build:
dotnet builddeve compilar sem erros nem warnings de source generators - Testes:
dotnet test— todos os testes existentes devem passar (MVVM nao muda services) - Code-behind limpo: Cada
.xaml.csdeve ter apenas:InitializeComponent()DataContext = viewModel(ou atribuicao via DI)- Event handlers de UI-only (ex: Window closing, drag behavior)
- Atualizar CLAUDE.md: Atualizar descricao do stack e arquitetura do projeto
- Funcionalidade: Testar manualmente que a UI funciona como antes
- Testes de ViewModel: Criar testes xUnit para o novo ViewModel (ver secao abaixo)
Testes de ViewModel (Recomendacao)
Cada migracao MVVM deve incluir testes de ViewModel. Eles sao a melhor forma de blindar o projeto contra regressoes durante refatoracoes — testam toda a logica de apresentacao sem abrir janelas, sao rapidos e confiaveis no CI.
O que testar
| Aspecto | Exemplo |
|---|---|
| Estado inicial | Propriedades iniciam com valores default corretos |
| Commands executam | CarregarCommand.Execute() popula propriedades |
| CanExecute | Botao desabilitado quando pre-condicao nao e atendida |
| Validacao | Dados invalidos mostram erro, nao executam acao |
Padrao para Commands com Dialogs
Commands que abrem OpenFileDialog nao sao testaveis unitariamente. Extraia a logica
para um metodo publico testavel:
// No ViewModel — o Command chama o dialog e depois o metodo testavel
[RelayCommand]
private void CarregarHardwareId()
{
var dialog = new Microsoft.Win32.OpenFileDialog { Filter = "*.hid" };
if (dialog.ShowDialog() == true)
PopularCampos(_service.RecuperarDeArquivo(dialog.FileName));
}
// Metodo publico testavel (sem dialog)
public void PopularCampos(HardwareInfo hwInfo)
{
CompanyName = hwInfo.CompanyName;
ProcessorId = hwInfo.ProcessorID;
// ...
IsSaveEnabled = true;
}
// No teste
[Fact]
public void PopularCampos_AtualizaPropriedadesEHabilitaSave()
{
var vm = new MainWindowViewModel(service);
vm.PopularCampos(new HardwareInfo { CompanyName = "JRC" });
Assert.Equal("JRC", vm.CompanyName);
Assert.True(vm.IsSaveEnabled);
}
Cuidado com testes que usam reflection
Testes que acessam metodos privados via typeof(Page).GetMethod("NomeMetodo", BindingFlags.NonPublic)
quebrarao quando o metodo for movido do code-behind para o ViewModel. O typeof precisa ser
atualizado de typeof(MinhaPage) para typeof(MinhaPageViewModel). Identifique esses testes
ANTES de mover codigo — consulte a checklist pre-migracao.
Testes E2E (para projetos maiores)
Para smoke tests visuais em projetos com muitas telas, considere FlaUI
(framework de automacao UI para WPF). Veja TODO_SPECS/SPEC-Automated-UI-Testing.md
para o plano completo.
Cenarios Comuns
Projeto WPF com code-behind (sem MVVM)
Este e o cenario mais comum — o projeto ja e WPF mas usa event handlers diretamente. Execute todos os 6 passos. O Passo 4 e o mais trabalhoso: extrair logica dos event handlers para ViewModels.
Projeto WinForms (migrar para WPF + MVVM)
Leia references/migration-winforms-to-wpf.md antes de comecar.
A migracao acontece em duas fases:
- Fase A: Converter Form para Window/Page (XAML equivalente ao layout do Form)
- Fase B: Aplicar MVVM (Passos 3-6 deste workflow)
Migre form-a-form usando Strangler Fig pattern. Nao migre tudo de uma vez.
Novo projeto WPF do zero
Leia references/project-structure.md para a estrutura de pastas recomendada.
Crie a estrutura Models/Views/ViewModels/Services antes de comecar a codar.
Comece pelo Passo 2 (pacotes), pule para Passo 3 (DI), depois crie ViewModels e Views.
Adicionar navegacao entre paginas
Leia references/wpfui-integration.md secao sobre NavigationView.
Use INavigationService + IPageService do WPF-UI para navegacao DI-friendly.
Detalhes Criticos
Classes DEVEM ser
partial— source generators do CommunityToolkit exigempartial class. Sempartial,[ObservableProperty]e[RelayCommand]nao geram codigo e o build falha.Campos
[ObservableProperty]devem serprivate— o generator cria a propriedade publica a partir do nome do campo:_nomeDoNaviogeraNomeDoNavio. Se o campo for publico, conflita.Nao misturar dialogs WinForms e WPF — em projetos WPF, usar
Microsoft.Win32.OpenFileDialogeMicrosoft.Win32.SaveFileDialog, nao os equivalentes de System.Windows.Forms.ObservableCollection<T>nao precisa de[ObservableProperty]— declare como propriedade publica simples:public ObservableCollection<Item> Items { get; } = new();. A collection ja implementaINotifyCollectionChangedinternamente.Atualizar CLAUDE.md apos migrar — referencias a Form*.cs ficam desatualizadas apos migracao. Atualizar descricao do stack, nomes de arquivos UI, e tabela de projetos.
CanExecute com
[RelayCommand]— para habilitar/desabilitar botoes automaticamente, use[RelayCommand(CanExecute = nameof(PodeSalvar))]e chameSalvarCommand.NotifyCanExecuteChanged()quando a condicao mudar.Async commands cancelam automaticamente — se o metodo retorna
Task, o[RelayCommand]geraIAsyncRelayCommandque desabilita o botao durante execucao e suporta cancelamento.Inicializar campos string com
= string.Empty— campos[ObservableProperty]do tipo string devem ser inicializados:private string _nome = string.Empty;. Sem isso, bindings podem receber null e causar warnings ou comportamento inesperado.StatusMessage como alternativa a MessageBox — para apps simples (1-2 telas), substituir
MessageBox.Show()por atualizar uma propriedadeStatusMessageno ViewModel e exibi-la na barra de status e mais simples e testavel que criarIDialogService. ReservarIContentDialogServicedo WPF-UI para apps com multiplas telas ou dialogs complexos.Icone da aplicacao em FluentWindow — nao usar
Icon=no XAML nemBitmapImageno code-behind (ambos carregam o menor frame do .ico e ficam pixelados). UsarBitmapDecoderpara selecionar o frame de maior resolucao. Declarar o .ico como<ApplicationIcon>E<Resource>no .csproj. Vejareferences/wpfui-integration.mdsecao "Icone da Aplicacao".ui:PageNAO existe no WPF-UI 4.2.0 — usar<Page>padrao do WPF (namespaceSystem.Windows.Controls). Code-behind herdaPage, NAOINavigableView<T>.INavigationViewPageProvideresta emWpf.Ui.Abstractions— NAO emWpf.UinemWpf.Ui.Controls. Metodo:GetPage(Type pageType)retornaobject?.MessageBoxButtonconflita com WPF-UI — quando ambos namespaces sao usados, adicionar alias:using MessageBoxButton = System.Windows.MessageBoxButton;eusing MessageBoxImage = System.Windows.MessageBoxImage;.NAO importar
Wpf.Ui.Controlsglobalmente — causa conflitos comMessageBoxButton,Page, etc. Qualificar tipos WPF-UI individualmente:public partial class MainWindow : Wpf.Ui.Controls.FluentWindow.PageService deve ser criado manualmente — WPF-UI nao fornece implementacao built-in de
INavigationViewPageProvider. Criar classePageService(IServiceProvider sp)comGetPage(Type) => sp.GetService(pageType). Setup:RootNavigation.SetPageProviderService(pageProvider)(NAOSetPageService()).NavigationView action items: usar
PreviewMouseLeftButtonUp— no WPF-UI 4.2.0, nemItemInvokednemSelectionChangeddisparam para NavigationViewItems semTargetPageType(items de acao como Browse/Upload). UsarPreviewMouseLeftButtonUpdiretamente no NavigationViewItem come.Handled = true.DataGrid: usar
AutoGenerateColumns="False"— definir colunas explicitamente em XAML para controlar visibilidade, headers e formatacao. Colunas que nao devem aparecer simplesmente nao sao declaradas (mais limpo queVisibility="Collapsed"em cada coluna).NavigationView quebra virtualizacao — o NavigationView do WPF-UI internamente usa layout que da altura infinita as paginas. Qualquer ListBox/DataGrid/ListView dentro de uma Page recebe ActualHeight infinito e renderiza TODOS os items (virtualizacao desabilitada). Fix obrigatorio: usar
MaxHeightfixo +Page_SizeChangedpara ajustar dinamicamente:private void Page_SizeChanged(object sender, SizeChangedEventArgs e) { if (dgvLog != null && e.NewSize.Height > 100) dgvLog.MaxHeight = e.NewSize.Height - 120; }Singleton para paginas pesadas — Pages registradas como
Transientsao recriadas a cada navegacao (visual tree, bindings, tudo reconstruido). Para paginas com dados grandes, registrar comoSingletonno DI evita reconstrucao e mantem estado de scroll/filtro. Adicionar metodoReloadData()para resetar quando novos dados sao carregados.WindowBackdropType="None" com WindowsFormsHost —
Micahabilita transparencia internamente, o que torna controles WinForms (via WindowsFormsHost) invisiveis. Bug documentado pela Microsoft. UsarWindowBackdropType="None"se WindowsFormsHost for necessario.Page.Resources ANTES do conteudo — declarar
<Page.Resources>com Styles/converters ANTES do conteudo XAML (DockPanel, Grid, etc). Se declarado depois,StaticResourcefalha com erro "StaticResourceExtension" em runtime.SolidColorBrush.Freeze() — brushes estaticos devem ser frozen para thread-safety:
private static SolidColorBrush CreateFrozenBrush(byte r, byte g, byte b) { var brush = new SolidColorBrush(Color.FromRgb(r, g, b)); brush.Freeze(); return brush; }LINQ filter em POCOs em vez de DataView.RowFilter — para listas grandes (100K+), converter DataTable para
List<T>tipado em background e filtrar com LINQ e mais rapido e thread-safe que DataView.RowFilter (que usa reflexao e nao e thread-safe).Debounce para filtros — em TextBoxes de filtro, usar debounce de 300ms com
CancellationTokenSourcepara filtrar enquanto o usuario digita sem travar a UI:_filterCts?.Cancel(); _filterCts?.Dispose(); _filterCts = new CancellationTokenSource(); _ = Task.Delay(300, _filterCts.Token).ContinueWith(t => { if (!t.IsCanceled) Dispatcher.Invoke(ApplyFilter); });Lazy property caching com ??= — para propriedades formatadas chamadas repetidamente pelo binding (ex: DateTimeFormatted), usar lazy initialization para evitar ToString() em cada frame de renderizacao:
private string? _formatted; public string Formatted => _formatted ??= DateTime.ToString("dd/MM/yyyy HH:mm:ss");IReadOnlyList para caches — expor caches estaticos como
IReadOnlyList<T>em vez deList<T>para prevenir modificacao acidental por consumidores.Lifecycle mismatch: Transient VM + Singleton Service = memory leak — se um ViewModel registrado como Transient assina
PropertyChangedde um servico Singleton (ex: IAppStateService), cada navegacao cria uma nova instancia que nunca e dessubscrita. O Singleton mantem delegate references para instancias mortas, impedindo o GC. Em apps com NavigationView, onde paginas sao recriadas a cada navegacao, isso causa leak cumulativo. Fix preferido: registrar ViewModels como Singleton (consistente com Detalhe #19 sobre paginas pesadas). Alternativas: implementarIDisposablecom unsubscribe, ou usarWeakEventManager(mas este requerSystem.Windowsque viola a separacao ViewModel/UI).Visibility bindings esquecidos ao migrar handlers — ao converter Click handlers que alternavam
Visibilityde paineis para Commands no ViewModel, e comum criar as propriedadesIsXxxVisibleno VM mas esquecer de adicionarVisibility="{Binding IsXxxVisible, Converter={StaticResource BoolToVis}}"no XAML. O resultado e que os Commands executam mas nada muda visualmente. Sempre auditar o XAML apos converter handlers de visibilidade.
Anti-padroes desta Skill
- ViewModel referenciando UI — ViewModel NUNCA deve importar
System.Windowsou acessar controles da View. Use bindings e Messenger para tudo. - Logica de negocio no ViewModel — ViewModel orquestra, Service executa. Se o ViewModel esta fazendo IO, parsing ou calculo complexo, mova para um Service.
new ViewModel()no XAML — funciona, mas impede DI. Prefira injetar via construtor.- Ignorar UpdateSourceTrigger —
TextBoxdefault eLostFocus. UseUpdateSourceTrigger=PropertyChangedpara validacao em tempo real. List<T>em vez deObservableCollection<T>—Listnao notifica a View quando itens sao adicionados/removidos. Sempre useObservableCollectionpara listas bindadas.- DataGrid/ListView para listas grandes (>5K items) — WPF DataGrid e ListView travam dentro de NavigationView mesmo com virtualizacao. Usar ListBox com paginacao (500 items/pagina) ou registrar a Page como Singleton. Nunca confiar apenas na virtualizacao sem testar.
- DataView.RowFilter em background thread — DataView NAO e thread-safe. Usar LINQ em
List<T>tipado ou copiar o DataTable antes de filtrar.DefaultViewcompartilhado entre consumidores causa race conditions. - SymbolIcons inexistentes — nem todos os icones listados na documentacao do WPF-UI existem
na versao 4.2.0. Exemplos que NAO existem:
SignalStrength24,PlugConnected24. Testar em runtime antes de commitar. async voidem metodos que nao sao event handlers — metodosasync voidfora de handlers UI (Click, Loaded) causam excecoes nao-observadas que podem crashar a aplicacao. Sempre usarasync Taskeawaitno chamador.[RelayCommand]geraIAsyncRelayCommandque ja usaasync Taskinternamente — nunca converter paraasync void.using Wpf.Ui.Controls;global — conflita comSystem.Windows.Controls(TextBox, ComboBox, Page, Button). Usar type aliases:using ControlAppearance = Wpf.Ui.Controls.ControlAppearance;- ContentDialogPresenter no XAML — o elemento correto e
<ui:ContentDialogHost>, nao<ui:ContentPresenter>ou<ui:ContentDialogPresenter>. Erro comum que causa crash. - ShowSimpleDialogAsync sem using —
ShowSimpleDialogAsynce extension method emWpf.Ui.Extensions. Requerusing Wpf.Ui.Extensions;no arquivo. new Service()dentro do ViewModel — ViewModel NAO deve instanciar servicos diretamente. Use injecao de construtor. Se o service e thin wrapper (ex:new UsuariosServicos(repo)), injete a interface subjacente diretamente (IUsuariosRepositorio) e chame_repo.Salvar(). Instanciar services no VM impede mocking nos testes e viola o principio de inversao de dependencias.- Remover error handling ao migrar handlers — handlers de Click frequentemente tem
try/catchcomMessageBox.Show()no catch. Ao migrar para[RelayCommand], e facil esquecer o error path. O resultado e que falhas sao engolidas silenciosamente (o usuario nao recebe feedback). Sempre preservar error handling: useStatusMessageproperty ouIContentDialogServiceno catch.
Checklist Pre-Migracao de Pagina
Antes de migrar cada Page para MVVM, audite o code-behind e verifique:
- Event handlers — listar todos (Click, Loaded, TextChanged, SelectionChanged, KeyDown)
- Visibilidade por codigo —
element.Visibility = Visible/Collapsed→ precisara de binding comBooleanToVisibilityConverter. Facil de esquecer (ver Detalhe #28) - ComboBox com selecao logica — se a selecao do ComboBox afeta comportamento (ex: tipo
de filtro), precisa de binding ou
SelectionChangedhandler que atualiza o ViewModel - Error handling em handlers —
try/catchcom MessageBox → preservar no ViewModel comStatusMessageouIContentDialogService(nao remover silenciosamente) - Custom controls imperativos — controles com API
GetValue()/SetValue()/SetDate()sem DependencyProperties → nao suportam binding (ver secao Custom Controls abaixo) - Operacoes visuais —
ScrollToTop(),Focus(),Mouse.OverrideCursor→ manter em code-behind como excecao documentada (SC-002 exception) - Testes com reflection — testes que usam
typeof(Page).GetMethod()para metodos privados quebrarao quando o metodo for movido para o ViewModel. Atualizartypeofapos mover
Estado Compartilhado (IAppStateService)
Para apps com multiplas paginas que compartilham estado (ex: dados carregados, modo de operacao,
filtros ativos), um servico Singleton com INotifyPropertyChanged e mais simples e direto que
IMessenger (WeakReferenceMessenger):
public interface IAppStateService : INotifyPropertyChanged
{
VDR? Vdr { get; }
bool IsVdrLoaded { get; }
bool ModoCoCAtivado { get; }
string SelectedPath { get; }
void CarregarVdr(VDR vdr, string path, bool modoCoc);
}
Quando usar IAppStateService vs IMessenger:
| Cenario | Padrao |
|---|---|
| Estado central que multiplos VMs leem | IAppStateService (Singleton + INotifyPropertyChanged) |
| Evento pontual entre VMs sem estado | IMessenger (WeakReferenceMessenger) |
| Notificacao de navegacao | IMessenger |
| Dados de sessao (usuario logado, modo) | IAppStateService |
Regra critica: se ViewModels assinam PropertyChanged de um servico Singleton, registrar
os VMs tambem como Singleton para evitar memory leak (ver Detalhe #27).
Testabilidade: IAppStateService e facilmente mockavel com NSubstitute:
var appState = Substitute.For<IAppStateService>();
appState.IsVdrLoaded.Returns(true);
appState.Vdr.Returns(new VDR1800());
var vm = new ChannelsPageViewModel(appState);
Custom Controls e Data Binding
Se o projeto usa UserControls custom (ex: controles de formulario especializados como APTCheckBoxWPF, AptDateWPF), audite ANTES de planejar a migracao:
Verificar DependencyProperties — o controle expoe DP para seu valor principal?
grep -r "DependencyProperty" VDAControls/WPF/Se retorna vazio, o controle nao suporta data binding.
API imperativa = sem binding — se o controle usa
GetValue()/SetValue()/SetDate()/GetDay()em vez de DependencyProperties, data binding bidirecional e impossivel.Abordagem pragmatica para migracao:
- ViewModel gerencia Commands e estado de visibilidade (funciona sem DP)
- Code-behind mantem mapeamento imperativo (FillForm/GetForm) como excecao documentada
- Planejar spec separada para adicionar DependencyProperties aos custom controls
- Quando DPs estiverem prontas, substituir code-behind por binding no XAML
Adicionar DependencyProperties (spec separada) — cada controle precisa de pelo menos uma DP para seu valor principal. Exemplo para um checkbox custom:
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(string), typeof(APTCheckBoxWPF), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged));
Guias de Referencia (progressive disclosure level 3)
Leia estes arquivos somente quando necessario no passo correspondente:
| Arquivo | Leia quando... |
|---|---|
references/mvvm-fundamentals.md |
Usuario e novo em MVVM ou quer entender conceitos |
references/communitytoolkit-patterns.md |
Passo 4 — criando ViewModels com source generators |
references/wpfui-integration.md |
Passo 3 — configurando DI, navegacao e theming com WPF-UI |
references/migration-winforms-to-wpf.md |
Projeto e WinForms e precisa migrar para WPF |
references/project-structure.md |
Criando projeto do zero ou reorganizando pastas |