---
name: dotnet-wpf-modern
description: Builds WPF on .NET 8+. Host builder, MVVM Toolkit, Fluent theme, performance, modern C# patterns.
license: MIT
user-invocable: false
---
# dotnet-wpf-modern
WPF on .NET 8+: Host builder and dependency injection, MVVM with CommunityToolkit.Mvvm source generators, hardware-accelerated rendering improvements, modern C# patterns (records, primary constructors, pattern matching), Fluent theme (.NET 9+), system theme detection, and what changed from .NET Framework WPF.
**Version assumptions:** .NET 8.0+ baseline (current LTS). TFM `net8.0-windows`. .NET 9 features (Fluent theme) explicitly marked.
## Scope
- WPF .NET 8+ project setup (SDK-style)
- Host builder and dependency injection
- MVVM with CommunityToolkit.Mvvm source generators
- Fluent theme (.NET 9+) and system theme detection
- Hardware-accelerated rendering improvements
- Modern C# patterns (records, primary constructors, pattern matching)
## Out of scope
- WPF .NET Framework patterns (legacy)
- Migration guidance -- see [skill:dotnet-wpf-migration]
- Desktop testing -- see [skill:dotnet-ui-testing-core]
- General Native AOT patterns -- see [skill:dotnet-native-aot]
- UI framework selection -- see [skill:dotnet-ui-chooser]
Cross-references: [skill:dotnet-ui-testing-core] for desktop testing, [skill:dotnet-winui] for WinUI 3 patterns, [skill:dotnet-wpf-migration] for migration guidance, [skill:dotnet-native-aot] for general AOT, [skill:dotnet-ui-chooser] for framework selection, [skill:dotnet-accessibility] for accessibility patterns (AutomationProperties, AutomationPeer, UI Automation).
---
## .NET 8+ Differences
WPF on .NET 8+ is a significant modernization from .NET Framework WPF. The project format, DI pattern, language features, and runtime behavior have all changed.
### New Project Template
```xml
WinExenet8.0-windowstrueenableenable
```
**Key differences from .NET Framework WPF:**
- SDK-style `.csproj` (no `packages.config`, no `AssemblyInfo.cs`)
- Nullable reference types enabled by default
- Implicit usings enabled
- NuGet `PackageReference` format (not `packages.config`)
- No `App.config` for DI -- use Host builder
- `dotnet publish` produces a single deployment artifact
- Side-by-side .NET installation (no machine-wide framework dependency)
### Host Builder Pattern
Modern WPF apps use the generic host for dependency injection, configuration, and logging -- replacing the legacy `ServiceLocator` or manual DI approaches.
```csharp
// App.xaml.cs
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) =>
{
// Services
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
// HTTP client
services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri(
context.Configuration["ApiBaseUrl"] ?? "https://api.example.com");
});
// ViewModels
services.AddTransient();
services.AddTransient();
services.AddTransient();
// Windows and pages
services.AddSingleton();
})
.Build();
}
protected override async void OnStartup(StartupEventArgs e)
{
await _host.StartAsync();
var mainWindow = _host.Services.GetRequiredService();
mainWindow.Show();
base.OnStartup(e);
}
protected override async void OnExit(ExitEventArgs e)
{
await _host.StopAsync();
_host.Dispose();
base.OnExit(e);
}
public static T GetService() where T : class
{
var app = (App)Application.Current;
return app._host.Services.GetRequiredService();
}
}
```
---
## MVVM Toolkit
CommunityToolkit.Mvvm (Microsoft MVVM Toolkit) is the recommended MVVM framework for modern WPF. It uses source generators to eliminate boilerplate.
```csharp
// ViewModels/ProductListViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class ProductListViewModel : ObservableObject
{
private readonly IProductService _productService;
public ProductListViewModel(IProductService productService)
{
_productService = productService;
}
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SearchCommand))]
private string _searchTerm = "";
[ObservableProperty]
private ObservableCollection _products = [];
[ObservableProperty]
private bool _isLoading;
[RelayCommand]
private async Task LoadProductsAsync(CancellationToken ct)
{
IsLoading = true;
try
{
var items = await _productService.GetProductsAsync(ct);
Products = new ObservableCollection(items);
}
finally
{
IsLoading = false;
}
}
[RelayCommand(CanExecute = nameof(CanSearch))]
private async Task SearchAsync(CancellationToken ct)
{
var results = await _productService.SearchAsync(SearchTerm, ct);
Products = new ObservableCollection(results);
}
private bool CanSearch() => !string.IsNullOrWhiteSpace(SearchTerm);
}
```
### XAML Binding with MVVM Toolkit
```xml
```
**Key source generator attributes:**
- `[ObservableProperty]` -- generates property with `INotifyPropertyChanged` from a backing field
- `[RelayCommand]` -- generates `ICommand` from a method (supports async, cancellation, `CanExecute`)
- `[NotifyPropertyChangedFor]` -- raises `PropertyChanged` for dependent properties
- `[NotifyCanExecuteChangedFor]` -- re-evaluates command `CanExecute` when property changes
---
## Performance
WPF on .NET 8+ delivers significant performance improvements over .NET Framework WPF.
### Hardware-Accelerated Rendering
- **DirectX 11 rendering path** is the default on .NET 8+ (up from DirectX 9 on .NET Framework)
- **GPU-accelerated text rendering** improves text clarity and reduces CPU usage for text-heavy UIs
- **Reduced GC pressure** from runtime improvements (dynamic PGO, on-stack replacement)
### Startup Time
- **ReadyToRun (R2R)** -- pre-compiled assemblies reduce JIT overhead at startup
- **Tiered compilation** -- fast startup with progressive optimization
- **Trimming readiness** -- `.NET 8+` WPF supports IL trimming for smaller deployment size
```xml
truepartial
```
**Trimming caveat:** WPF relies heavily on XAML reflection for data binding and resource resolution. Use `TrimMode=partial` (not `full`) and test thoroughly. Compiled bindings and `x:Type` references are safer than string-based bindings for trimming.
### Memory and GC
- **Frozen object heap** (.NET 8) -- static strings and singleton allocations placed on non-collected heap segments
- **Dynamic PGO** -- runtime profiles guide JIT optimizations for hot paths
- **Reduced working set** -- .NET 8 runtime uses less baseline memory than .NET Framework CLR
### Expected Improvements
WPF on .NET 8 delivers measurable improvements over .NET Framework 4.8 across key metrics. Exact numbers depend on workload, hardware, and application complexity -- always benchmark your own scenarios:
- **Cold startup** -- significantly faster due to ReadyToRun, tiered compilation, and reduced framework initialization overhead
- **UI virtualization** -- improved rendering pipeline and GC reduce time for large ItemsControls (ListBox, DataGrid)
- **GC pauses** -- shorter and less frequent Gen2 collections from .NET 8 GC improvements (Dynamic PGO, frozen object heap, pinned object heap)
- **Memory footprint** -- lower baseline working set compared to .NET Framework CLR
---
## Modern C#
.NET 8+ WPF projects can use the latest C# language features. These patterns reduce boilerplate and improve code clarity.
### Records for Data Models
```csharp
// Immutable data models
public record Product(string Name, decimal Price, string Category);
// Records with computed properties
public record ProductViewModel(Product Product)
{
public string DisplayPrice => Product.Price.ToString("C");
public string Summary => $"{Product.Name} - {DisplayPrice}";
}
```
### Primary Constructors in Services
```csharp
// Service with primary constructor (C# 12)
public class ProductService(HttpClient httpClient, ILogger logger)
: IProductService
{
public async Task> GetProductsAsync(CancellationToken ct)
{
logger.LogInformation("Fetching products");
var response = await httpClient.GetAsync("/products", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync>(ct) ?? [];
}
}
```
### Pattern Matching in Converters
```csharp
// Modern converter using pattern matching (C# 11+)
public class StatusToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value switch
{
OrderStatus.Pending => Brushes.Orange,
OrderStatus.Processing => Brushes.Blue,
OrderStatus.Shipped => Brushes.Green,
OrderStatus.Cancelled => Brushes.Red,
_ => Brushes.Gray
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
```
### Collection Expressions
```csharp
// C# 12 collection expressions
[ObservableProperty]
private ObservableCollection _products = [];
// In methods
List categories = ["Electronics", "Clothing", "Books"];
```
---
## Theming
### Fluent Theme (.NET 9+)
.NET 9 introduces the Fluent theme for WPF, providing modern Windows 11-style visuals. It applies rounded corners, updated control templates, and Mica/Acrylic backdrop support.
```xml
```
Or in code-behind:
```csharp
// App.xaml.cs: set theme programmatically (.NET 9+)
Application.Current.ThemeMode = ThemeMode.System; // or ThemeMode.Light / ThemeMode.Dark
// Per-window theming is also supported
mainWindow.ThemeMode = ThemeMode.Dark;
```
**ThemeMode values:**
- `None` -- classic WPF look (no Fluent styling)
- `Light` -- Fluent theme with light colors
- `Dark` -- Fluent theme with dark colors
- `System` -- follow Windows system light/dark theme setting
**Fluent theme includes:**
- Rounded corners on buttons, text boxes, and list items
- Updated color palette aligned with Windows 11 design language
- Mica and Acrylic backdrop support (Windows 11)
- Accent color integration with Windows system settings
- Dark/light mode following system theme
### System Theme Detection
Detect and respond to the Windows system light/dark theme:
```csharp
// Detect system theme
public static bool IsDarkTheme()
{
using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
var value = key?.GetValue("AppsUseLightTheme");
return value is int i && i == 0;
}
// Listen for theme changes
SystemEvents.UserPreferenceChanged += (sender, args) =>
{
if (args.Category == UserPreferenceCategory.General)
{
// Theme may have changed; re-read and apply
ApplyTheme(IsDarkTheme() ? AppTheme.Dark : AppTheme.Light);
}
};
```
### Custom Themes
For pre-.NET 9 apps or custom branding, use resource dictionaries:
```xml
```
```csharp
// Switch themes at runtime
public void ApplyTheme(AppTheme theme)
{
var themeUri = theme switch
{
AppTheme.Dark => new Uri("Themes/DarkTheme.xaml", UriKind.Relative),
AppTheme.Light => new Uri("Themes/LightTheme.xaml", UriKind.Relative),
_ => throw new ArgumentOutOfRangeException(nameof(theme))
};
Application.Current.Resources.MergedDictionaries.Clear();
Application.Current.Resources.MergedDictionaries.Add(
new ResourceDictionary { Source = themeUri });
}
```
---
## Agent Gotchas
1. **Do not use .NET Framework WPF patterns in .NET 8+ projects.** Avoid `App.config` for DI (use Host builder), `packages.config` (use `PackageReference`), `ServiceLocator` pattern (use constructor injection), and `AssemblyInfo.cs` (use `` properties).
2. **Do not use deprecated WPF APIs.** `BitmapEffect` (replaced by `Effect`/`ShaderEffect`), `DrawingContext.PushEffect` (removed), and `VisualBrush` tile modes with hardware acceleration disabled are obsolete.
3. **Do not mix `{Binding}` and manual `INotifyPropertyChanged` when using MVVM Toolkit.** Use `[ObservableProperty]` source generators consistently. Mixing approaches causes subtle binding update bugs.
4. **Do not use `Dispatcher.Invoke` from async code.** In async methods, `await` automatically marshals back to the UI thread (the default `ConfigureAwait(true)` behavior). `Dispatcher.Invoke`/`BeginInvoke` is still appropriate from non-async contexts (timers, COM callbacks, native interop).
5. **Do not set `TrimMode=full` for WPF apps.** WPF uses XAML reflection extensively. Use `TrimMode=partial` and test all views after trimming to catch missing types.
6. **Do not forget the Host builder lifecycle.** Call `_host.StartAsync()` in `OnStartup` and `_host.StopAsync()` in `OnExit`. Forgetting lifecycle management causes DI-registered `IHostedService` instances to never start or stop.
7. **Do not hardcode colors when using Fluent theme.** Reference theme resources (`{DynamicResource SystemAccentColor}`) to maintain compatibility with light/dark mode and system accent color changes.
---
## Prerequisites
- .NET 8.0+ with Windows desktop workload
- TFM: `net8.0-windows` (no Windows SDK version needed for WPF)
- Visual Studio 2022+, VS Code with C# Dev Kit, or JetBrains Rider
- For Fluent theme: .NET 9+
---
## References
- [WPF on .NET Documentation](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/)
- [CommunityToolkit.Mvvm](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/)
- [WPF Fluent Theme (.NET 9)](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/whats-new/net90)
- [Microsoft.Extensions.Hosting](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host)
- [WPF Performance](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/optimizing-performance)