Skill v1.0.1
currentAutomated scan100/1002 files
version: "1.0.1" name: dotnet-wpf-modern description: "Building WPF on .NET 8+. Host builder, MVVM Toolkit, Fluent theme, performance, modern C# patterns."
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 boundary: This skill owns WPF on modern .NET patterns: Host builder, MVVM Toolkit, performance, modern C#, theming. Migration from .NET Framework to .NET 8+ is owned by [skill:dotnet-wpf-migration]. Desktop testing is owned by [skill:dotnet-ui-testing-core].
Out of scope: WPF .NET Framework patterns (legacy) -- this skill covers .NET 8+ only. 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
<!-- MyWpfApp.csproj (SDK-style) --><Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>WinExe</OutputType><TargetFramework>net8.0-windows</TargetFramework><UseWPF>true</UseWPF><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup><ItemGroup><PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" /><PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" /></ItemGroup></Project>
Key differences from .NET Framework WPF:
- SDK-style
.csproj(nopackages.config, noAssemblyInfo.cs) - Nullable reference types enabled by default
- Implicit usings enabled
- NuGet
PackageReferenceformat (notpackages.config) - No
App.configfor DI -- use Host builder dotnet publishproduces 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.
// App.xaml.cspublic 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) =>{// Servicesservices.AddSingleton<INavigationService, NavigationService>();services.AddSingleton<IProductService, ProductService>();services.AddSingleton<ISettingsService, SettingsService>();// HTTP clientservices.AddHttpClient("api", client =>{client.BaseAddress = new Uri(context.Configuration["ApiBaseUrl"] ?? "https://api.example.com");});// ViewModelsservices.AddTransient<MainViewModel>();services.AddTransient<ProductListViewModel>();services.AddTransient<SettingsViewModel>();// Windows and pagesservices.AddSingleton<MainWindow>();}).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);}public static T GetService<T>() where T : class{var app = (App)Application.Current;return app._host.Services.GetRequiredService<T>();}}
MVVM Toolkit
CommunityToolkit.Mvvm (Microsoft MVVM Toolkit) is the recommended MVVM framework for modern WPF. It uses source generators to eliminate boilerplate.
// ViewModels/ProductListViewModel.csusing 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<Product> _products = [];[ObservableProperty]private bool _isLoading;[RelayCommand]private async Task LoadProductsAsync(CancellationToken ct){IsLoading = true;try{var items = await _productService.GetProductsAsync(ct);Products = new ObservableCollection<Product>(items);}finally{IsLoading = false;}}[RelayCommand(CanExecute = nameof(CanSearch))]private async Task SearchAsync(CancellationToken ct){var results = await _productService.SearchAsync(SearchTerm, ct);Products = new ObservableCollection<Product>(results);}private bool CanSearch() => !string.IsNullOrWhiteSpace(SearchTerm);}
XAML Binding with MVVM Toolkit
<Window x:Class="MyApp.Views.ProductListWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:vm="clr-namespace:MyApp.ViewModels"d:DataContext="{d:DesignInstance vm:ProductListViewModel}"><DockPanel><StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="16"><TextBox Text="{Binding SearchTerm, UpdateSourceTrigger=PropertyChanged}"Width="300" Margin="0,0,8,0" /><Button Content="Search" Command="{Binding SearchCommand}" /></StackPanel><ListBox ItemsSource="{Binding Products}" Margin="16"><ListBox.ItemTemplate><DataTemplate><StackPanel Orientation="Horizontal" Margin="4"><TextBlock Text="{Binding Name}" FontWeight="Bold" Margin="0,0,12,0" /><TextBlock Text="{Binding Price, StringFormat='{}{0:C}'}" Foreground="Gray" /></StackPanel></DataTemplate></ListBox.ItemTemplate></ListBox></DockPanel></Window>
Key source generator attributes:
[ObservableProperty]-- generates property withINotifyPropertyChangedfrom a backing field[RelayCommand]-- generatesICommandfrom a method (supports async, cancellation,CanExecute)[NotifyPropertyChangedFor]-- raisesPropertyChangedfor dependent properties[NotifyCanExecuteChangedFor]-- re-evaluates commandCanExecutewhen 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
<!-- Enable trimming for smaller deployment --><PropertyGroup><PublishTrimmed>true</PublishTrimmed><TrimMode>partial</TrimMode><!-- WPF apps need partial trim mode due to reflection usage --></PropertyGroup>
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
// Immutable data modelspublic record Product(string Name, decimal Price, string Category);// Records with computed propertiespublic record ProductViewModel(Product Product){public string DisplayPrice => Product.Price.ToString("C");public string Summary => $"{Product.Name} - {DisplayPrice}";}
Primary Constructors in Services
// Service with primary constructor (C# 12)public class ProductService(HttpClient httpClient, ILogger<ProductService> logger): IProductService{public async Task<IReadOnlyList<Product>> GetProductsAsync(CancellationToken ct){logger.LogInformation("Fetching products");var response = await httpClient.GetAsync("/products", ct);response.EnsureSuccessStatusCode();return await response.Content.ReadFromJsonAsync<List<Product>>(ct) ?? [];}}
Pattern Matching in Converters
// 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
// C# 12 collection expressions[ObservableProperty]private ObservableCollection<Product> _products = [];// In methodsList<string> 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.
<!-- App.xaml: enable Fluent theme (.NET 9+) via ThemeMode property --><Application x:Class="MyApp.App"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"ThemeMode="System"StartupUri="MainWindow.xaml"></Application>
Or in code-behind:
// App.xaml.cs: set theme programmatically (.NET 9+)Application.Current.ThemeMode = ThemeMode.System; // or ThemeMode.Light / ThemeMode.Dark// Per-window theming is also supportedmainWindow.ThemeMode = ThemeMode.Dark;
ThemeMode values:
None-- classic WPF look (no Fluent styling)Light-- Fluent theme with light colorsDark-- Fluent theme with dark colorsSystem-- 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:
// Detect system themepublic 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 changesSystemEvents.UserPreferenceChanged += (sender, args) =>{if (args.Category == UserPreferenceCategory.General){// Theme may have changed; re-read and applyApplyTheme(IsDarkTheme() ? AppTheme.Dark : AppTheme.Light);}};
Custom Themes
For pre-.NET 9 apps or custom branding, use resource dictionaries:
<!-- Themes/DarkTheme.xaml --><ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><SolidColorBrush x:Key="WindowBackground" Color="#1E1E1E" /><SolidColorBrush x:Key="TextForeground" Color="#FFFFFF" /><SolidColorBrush x:Key="AccentBrush" Color="#0078D7" /></ResourceDictionary>
// Switch themes at runtimepublic 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
- Do not use .NET Framework WPF patterns in .NET 8+ projects. Avoid
App.configfor DI (use Host builder),packages.config(usePackageReference),ServiceLocatorpattern (use constructor injection), andAssemblyInfo.cs(use<PropertyGroup>properties). - Do not use deprecated WPF APIs.
BitmapEffect(replaced byEffect/ShaderEffect),DrawingContext.PushEffect(removed), andVisualBrushtile modes with hardware acceleration disabled are obsolete. - Do not mix `{Binding}` and manual `INotifyPropertyChanged` when using MVVM Toolkit. Use
[ObservableProperty]source generators consistently. Mixing approaches causes subtle binding update bugs. - Do not use `Dispatcher.Invoke` from async code. In async methods,
awaitautomatically marshals back to the UI thread (the defaultConfigureAwait(true)behavior).Dispatcher.Invoke/BeginInvokeis still appropriate from non-async contexts (timers, COM callbacks, native interop). - Do not set `TrimMode=full` for WPF apps. WPF uses XAML reflection extensively. Use
TrimMode=partialand test all views after trimming to catch missing types. - Do not forget the Host builder lifecycle. Call
_host.StartAsync()inOnStartupand_host.StopAsync()inOnExit. Forgetting lifecycle management causes DI-registeredIHostedServiceinstances to never start or stop. - 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+