Architecture

Modern WPF Architecture 2026: A Practical Guide Beyond Legacy

Build modern WPF apps with Generic Host, DI, CommunityToolkit.Mvvm, and .NET 9. Eliminate MVVM boilerplate and plan migration.

Who should read this

Summary: WPF shipped in 2006 but remains central to enterprise desktop applications in 2026. The problem is not “WPF itself” but “WPF code written in a 2010-era style.” Combining .NET 9 + Generic Host + CommunityToolkit.Mvvm brings ASP.NET Core-grade DI, configuration, and logging to the desktop while cutting MVVM boilerplate by 90%. This article covers architecture setup for new WPF projects and incremental modernization strategies for existing ones.

This article is for C# developers maintaining or building new WPF applications. It also serves as a reference for teams considering migration from .NET Framework 4.x to .NET 9.


WPF in 2026: not dead, not replaced

“Is WPF legacy?” — No. Microsoft actively supports WPF in .NET 9, and WPF’s share of the Windows desktop app market remains dominant. Financial trading terminals, medical imaging viewers, industrial control systems, and enterprise admin tools often have no realistic alternative to WPF.

WPF (.NET 9)WinUI 3MAUIAvalonia
Maturity 20 years, highly stableGrowing, partly incompleteGrowing, mobile-centricMature, cross-platform
Windows support Win7 through Win11Win10 1809+ onlyWin10+Win/Mac/Linux
Ecosystem Largest (Telerik, DevExpress, etc.)GrowingLimitedGrowing
Existing asset compat 100% XAML compatibleSimilar XAML, not compatibleSimilar XAML, not compatibleSimilar XAML, not compatible
DirectX integration DirectX 9/11DirectX 12 (WinAppSDK)Platform-dependentSkia-based
Best-fit scenario Enterprise LOB, legacy modernizationWindows-only new appsCross-platform mobile + desktopCross-platform desktop
As of 2026. WPF still leads in existing asset compatibility and ecosystem breadth.

Modern WPF architecture — the four core layers

Overall structure

MyApp.sln
├── MyApp/                          # WPF project (entry point)
│   ├── App.xaml / App.xaml.cs      # Generic Host integration
│   ├── Views/                      # XAML View files
│   ├── appsettings.json            # Configuration file
│   └── Hosting/                    # Host wiring code
├── MyApp.Core/                     # Pure C# library (no UI dependency)
│   ├── ViewModels/                 # CommunityToolkit.Mvvm ViewModels
│   ├── Models/                     # Domain models
│   ├── Services/                   # Business logic
│   └── Interfaces/                 # Service interfaces
└── MyApp.Tests/                    # Unit tests
    └── ViewModelTests/

Core principle: ViewModels and business logic live in a pure .NET project with no WPF references. This lets you run ViewModels in a console app or test project without WPF.


Layer 1: Generic Host integration — App.xaml.cs

Bridging WPF’s Application lifecycle with the .NET Generic Host lifecycle is the first step.

App.xaml.cs csharp
public partial class App : Application { private readonly IHost _host; public App() { _host = Host.CreateDefaultBuilder() .ConfigureAppConfiguration((context, config) => { config.AddJsonFile("appsettings.json", optional: true); }) .ConfigureServices((context, services) => { // ViewModels services.AddTransient<MainViewModel>(); services.AddTransient<SettingsViewModel>(); // Services services.AddSingleton<IDataService, DataService>(); services.AddSingleton<INavigationService, NavigationService>(); // Views (Windows are Transient) services.AddTransient<MainWindow>(); // HttpClient services.AddHttpClient<IApiClient, ApiClient>(); // Logging services.AddLogging(builder => { builder.AddDebug(); builder.AddSerilog(); }); }) .Build(); } protected override async void OnStartup(StartupEventArgs e) { await _host.StartAsync(); var mainWindow = _host.Services.GetRequiredService<MainWindow>(); mainWindow.Show(); base.OnStartup(e); } protected override async void OnExit(ExitEventArgs e) { await _host.StopAsync(); _host.Dispose(); base.OnExit(e); } }

Benefits of this pattern:

  • DI container — constructor injection for dependency management; no more direct instantiation with new
  • Configuration managementappsettings.json + environment-specific overrides (appsettings.Development.json)
  • Logging — standard ILogger<T> interface with Serilog/NLog plugins
  • HttpClientIHttpClientFactory pattern to prevent socket exhaustion

Layer 2: CommunityToolkit.Mvvm — eliminating boilerplate

CommunityToolkit.Mvvm (formerly Microsoft.Toolkit.Mvvm) is a source-generator-based MVVM library. Add attributes and the code is generated at compile time.

Traditional approach (manual INotifyPropertyChanged):

// 30 lines -- this much boilerplate for a single field and a single command
public class MainViewModel : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged(nameof(Name));
            }
        }
    }

    private ICommand _saveCommand;
    public ICommand SaveCommand => _saveCommand ??=
        new RelayCommand(Save, () => !string.IsNullOrEmpty(Name));

    private void Save() { /* ... */ }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

CommunityToolkit.Mvvm approach:

MainViewModel.cs csharp
// 10 lines -- same functionality, zero boilerplate public partial class MainViewModel : ObservableObject { [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] private string _name; [RelayCommand(CanExecute = nameof(CanSave))] private void Save() { /* ... */ } private bool CanSave() => !string.IsNullOrEmpty(Name); }

What the source generator creates automatically:

  • Name property (get/set + PropertyChanged event firing)
  • SaveCommand (IRelayCommand implementation)
  • CanExecute wiring (NotifyCanExecuteChangedFor)

Layer 3: Service layer — interface-based design

IDataService.cs csharp
// Interface: in the MyApp.Core project public interface IDataService { Task<IReadOnlyList<Customer>> GetCustomersAsync(CancellationToken ct = default); Task<Customer> GetCustomerByIdAsync(int id, CancellationToken ct = default); Task SaveCustomerAsync(Customer customer, CancellationToken ct = default); }
DataService.cs csharp
// Implementation: in MyApp.Core (or MyApp.Infrastructure) public class DataService : IDataService { private readonly IDbContextFactory<AppDbContext> _dbFactory; private readonly ILogger<DataService> _logger; public DataService( IDbContextFactory<AppDbContext> dbFactory, ILogger<DataService> logger) { _dbFactory = dbFactory; _logger = logger; } public async Task<IReadOnlyList<Customer>> GetCustomersAsync( CancellationToken ct = default) { await using var db = await _dbFactory.CreateDbContextAsync(ct); return await db.Customers .AsNoTracking() .OrderBy(c => c.Name) .ToListAsync(ct); } }

Key patterns:

  • Interface segregation — ViewModels depend only on IDataService; the implementation is injected by DI
  • Async-first — all I/O operations use async/await to prevent UI thread blocking
  • CancellationToken — allows canceling in-progress queries when the user navigates away
  • DbContextFactory — creates a fresh DbContext each time to avoid concurrency issues

Layer 4: View — keep XAML thin

MainWindow.xaml.cs csharp
public partial class MainWindow : Window { // This is the only code-behind the View needs public MainWindow(MainViewModel viewModel) { InitializeComponent(); DataContext = viewModel; } }

View principle: No business logic in code-behind. Only DataContext assignment and pure UI interactions (drag-and-drop, focus management — things that cannot be done in XAML) are allowed.


WPF screen transitions generally follow one of two patterns.

ContentControl swapWindow-based navigation
Structure Single Window + swap ContentControl.ContentSeparate Window per screen
Best-fit scenario Tab/sidebar SPA-style layoutDialogs, independent windows
DI integration INavigationService swaps ViewModelsWindow created via DI
State management Easy to control ViewModel lifetimeState lost when Window closes
Recommendation Suits most LOB appsSuits settings windows, modal dialogs
Most enterprise WPF apps benefit from the ContentControl swap pattern.

Testing: ViewModels tested without WPF

The greatest benefit of CommunityToolkit.Mvvm + interface-based design is that ViewModel unit tests require no WPF reference.

MainViewModelTests.cs csharp
public class MainViewModelTests { [Fact] public async Task LoadCustomers_SetsCustomerList() { // Arrange -- inject a mock service var mockService = Substitute.For<IDataService>(); mockService.GetCustomersAsync(default) .Returns(new[] { new Customer { Name = "Test" } }); var vm = new MainViewModel(mockService); // Act await vm.LoadCustomersCommand.ExecuteAsync(null); // Assert Assert.Single(vm.Customers); Assert.Equal("Test", vm.Customers[0].Name); } }

Modernizing an existing WPF project

.NET Framework to .NET 9 migration sequence

Phase 1: Build system conversion (low risk)

  • Convert .csproj to SDK-style (dotnet try-convert tool)
  • NuGet packages.config to PackageReference
  • Still targeting .NET Framework at this point

Phase 2: .NET 9 target switch (medium risk)

  • Change <TargetFramework>: net48 to net9.0-windows
  • Replace incompatible NuGet packages
  • WCF to gRPC/REST, Remoting to direct communication
  • Microsoft.Windows.Compatibility package for Win32 API compatibility

Phase 3: Architecture modernization (low risk, incremental)

  • Introduce Generic Host + DI (modify App.xaml.cs)
  • Convert ViewModels to CommunityToolkit.Mvvm one at a time
  • Extract service layer interfaces + register in DI
  • Migrate existing singletons/static classes to DI-managed services

Phase 4: Add tests (incremental)

  • Add unit tests starting with newly converted ViewModels
  • For legacy code, add tests only when making changes (Boy Scout Rule)

WPF vs full replacement: decision criteria

  • Win10+ only support is acceptable, and you need WinUI 3’s latest controls
  • Cross-platform is mandatory (Mac/Linux — choose Avalonia)
  • Mobile coverage is required (choose MAUI)
  • Little to no existing WPF code, making this effectively greenfield development

In all other cases, existing WPF + .NET 9 + Generic Host + CommunityToolkit.Mvvm is the most realistic 2026 choice.


Anti-patterns to avoid


Conclusion: WPF is not legacy — “WPF code written with legacy patterns” is legacy

The core stack for modern WPF architecture in 2026:

  1. .NET 9 — latest runtime, performance improvements, C# 13 syntax
  2. Generic Host — DI + Configuration + Logging integration
  3. CommunityToolkit.Mvvm — source generators eliminate MVVM boilerplate
  4. EF Core + DbContextFactory — async data access
  5. xUnit + NSubstitute — ViewModel unit testing

With this stack, you achieve the same level of structure, testability, and maintainability as an ASP.NET Core project — in a desktop app. You do not need to replace WPF. You need to change the structure of the code written on top of WPF.

Further reading