WPF is still Microsoft's most capable framework for rich, data-heavy Windows desktop applications, and it has quietly kept pace with every .NET release since .NET Core 3.0. On .NET 8, 9 and 10 it looks and behaves differently than the WPF you remember from .NET Framework: MVVM boilerplate is gone thanks to CommunityToolkit.Mvvm's source generators, dependency injection follows the same generic host pattern used by ASP.NET Core, and a new Fluent theme gives apps a Windows 11 look without a third-party control library. This guide covers building a modern WPF app end to end, the current state of the Fluent theme, performance tuning, and migrating an existing app off .NET Framework.
What Is WPF on Modern .NET?#
Windows Presentation Foundation is a retained-mode UI framework for Windows: you describe a tree of controls in XAML, WPF builds and owns the underlying render data, and it repaints through hardware-accelerated composition rather than you drawing pixels yourself. Styling, templating, animation, layout and data binding all sit on top of a shared DependencyObject/DependencyProperty system, which is what makes WPF's styling and binding so much more powerful than plain CLR properties on other UI stacks.
"Modern .NET" WPF is the same framework, open-sourced and ported to run on the cross-platform .NET runtime instead of .NET Framework's CLR. It ships as a Windows-only workload of the SDK: add <UseWPF>true</UseWPF> to an SDK-style project and target a Windows-flavored TFM such as net10.0-windows. It is available on .NET 8 (LTS), .NET 9 (STS, reaching end of support on November 10, 2026), .NET 10 (the current LTS, released November 2025) and .NET 11, which reached its first release candidate in September 2026 ahead of a November 2026 ship date. WPF on .NET Framework 4.x still exists and is still serviced for compatibility, but new capabilities, including the Fluent theme covered below, only land in WPF on modern .NET.
How WPF Works: XAML, the Visual Tree and Data Binding#
Every WPF window has a logical tree, the elements you wrote in XAML, and a visual tree, the expanded set of primitives each control template produces to actually render. Events route through both trees: RoutedEvents tunnel down from the root (PreviewMouseDown) and bubble back up (MouseDown), which is why a click handler on a Window can observe clicks on any descendant without wiring up every control individually.
Data binding connects a control's dependency property to a CLR property on whatever object sits in DataContext, resolved by reflection at binding time. A source that implements INotifyPropertyChanged pushes updates back into the UI; a target bound with Mode=TwoWay pushes edits back into the source. Commands implement ICommand and bind through Command="{Binding SaveCommand}", decoupling a button from the method it invokes. Unlike WinUI 3's x:Bind or .NET MAUI's compiled bindings, WPF bindings stay reflection-based; there is no compiled-binding option, so binding-heavy views pay a real, measurable cost that the tips later in this guide help control.
Getting Started: A Minimal WPF App on .NET 10#
Scaffold a project with dotnet new wpf -n InvoiceDesk, which produces an SDK-style project like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
</PropertyGroup>
</Project>A window is a XAML file paired with a code-behind partial class. Binding to a view model instead of code-behind members is what makes the MVVM pattern in the next section work:
<Window x:Class="InvoiceDesk.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Invoice Desk" Height="450" Width="800">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
Width="240" HorizontalAlignment="Left" />
<ListView Grid.Row="1" ItemsSource="{Binding Invoices}"
SelectedItem="{Binding SelectedInvoice}" />
</Grid>
</Window>MVVM with CommunityToolkit.Mvvm#
CommunityToolkit.Mvvm removes almost all MVVM boilerplate through Roslyn source generators, and the pattern is identical whether the view is WPF, WinUI 3 or .NET MAUI, because the toolkit has no dependency on any specific UI framework. Since toolkit 8.4, [ObservableProperty] annotates a partial property instead of a backing field, using the C# 14 field keyword that is the default language version for net10.0 targets:
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using InvoiceDesk.Models;
using InvoiceDesk.Services;
namespace InvoiceDesk.ViewModels;
public partial class InvoiceListViewModel(IInvoiceStore store) : ObservableObject
{
public ObservableCollection<Invoice> Invoices { get; } = [];
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SearchCommand))]
public partial string SearchText { get; set; } = string.Empty;
[ObservableProperty]
public partial Invoice? SelectedInvoice { get; set; }
[RelayCommand(CanExecute = nameof(CanSearch))]
private async Task SearchAsync(CancellationToken cancellationToken)
{
var results = await store.FindAsync(SearchText, cancellationToken);
Invoices.Clear();
foreach (var invoice in results)
{
Invoices.Add(invoice);
}
}
private bool CanSearch() => SearchText.Length >= 2;
}The generator strips the Async suffix, so SearchAsync becomes SearchCommand, and the generated AsyncRelayCommand disables itself while running, which stops a double-click from firing two overlapping searches. [NotifyCanExecuteChangedFor] re-evaluates CanSearch every time SearchText changes, so the search box enables the command only once the user has typed enough characters. For cross-view-model notifications, use the toolkit's WeakReferenceMessenger instead of raising custom events, which avoids the classic WPF memory leak of a view model outliving its view because an event handler held a strong reference.
Dependency Injection with the Generic Host#
WPF has no built-in host, but nothing stops you from using Microsoft.Extensions.Hosting, the same generic host that ASP.NET Core and worker services use. Remove StartupUri from App.xaml so no window is created automatically, then build and start the host from OnStartup:
using System.Windows;
using InvoiceDesk.Services;
using InvoiceDesk.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace InvoiceDesk;
public partial class App : Application
{
private readonly IHost _host;
public App()
{
var builder = Host.CreateApplicationBuilder();
builder.Services.AddSingleton<IInvoiceStore, SqlInvoiceStore>();
builder.Services.AddTransient<InvoiceListViewModel>();
builder.Services.AddTransient<MainWindow>();
_host = builder.Build();
}
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
await _host.StartAsync();
var window = _host.Services.GetRequiredService<MainWindow>();
window.Show();
}
protected override async void OnExit(ExitEventArgs e)
{
await _host.StopAsync();
_host.Dispose();
base.OnExit(e);
}
}Inject the view model into the window's constructor and set it as DataContext, rather than resolving it from a static App.Host field inside the window: that keeps the window testable and keeps the DI graph explicit. The dependency injection guide covers lifetimes, captive-dependency pitfalls and IOptions<T> binding that apply here unchanged; builder.Configuration and builder.Logging work exactly as they do in an ASP.NET Core app, so appsettings.json and environment-based configuration need no WPF-specific code.
The Fluent Theme#
.NET 9 shipped a new Fluent theme for WPF that gives apps light and dark modes and system accent-color support matching Windows 11, and Microsoft publishes a WPF Gallery app on the Microsoft Store to preview it. The theme ships as a resource dictionary at a pack URI, and the simplest way to apply it application-wide is to merge it into App.xaml:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>A second, newer API wraps the same theme behind a ThemeMode property on Application and Window, so you do not need to reference the resource dictionary directly. Its values are Light, Dark, System (follow the Windows setting) and None, which is the default and falls back to the classic Aero2 theme:
<Application x:Class="InvoiceDesk.App" ThemeMode="System" ...>Setting ThemeMode at the application level themes every window; setting it on an individual Window themes just that window, but only if the application-level value is still None. Reading or writing ThemeMode from code is still experimental and raises compiler error WPF0001 unless you suppress it, which is worth knowing before you try to toggle themes at runtime from a settings screen:
#pragma warning disable WPF0001 // ThemeMode is an experimental API in .NET 9/10
private void OnDarkModeToggled(object sender, RoutedEventArgs e)
{
Application.Current.ThemeMode = darkModeCheckBox.IsChecked == true
? ThemeMode.Dark
: ThemeMode.Light;
}
#pragma warning restore WPF0001The Windows 10 accent color is also available independently of the Fluent theme through SystemColors, exposing the base AccentColor plus three lighter and three darker shades (for example AccentColorLight1 and AccentColorDark1) as both Color and SolidColorBrush resources; bind to the brush keys with a DynamicResource so the UI updates live if the user changes their accent color.
.NET 10 kept building out Fluent coverage: DatePicker, GridSplitter, GridView, GroupBox, Hyperlink, Label, NavigationWindow, RichTextBox and TextBox all picked up Fluent styling, along with right-to-left layout and high-contrast fixes. Microsoft still describes Fluent styling as a work in progress, so test complex, less common controls (custom DataGrid templates in particular) before shipping a themed app, and expect further coverage in .NET 11. Separately, .NET 10 unified the WPF and Windows Forms clipboard implementation and began obsoleting BinaryFormatter-based clipboard APIs, continuing the removal of BinaryFormatter itself that started as a security fix in .NET 9.
Performance Tips#
- Virtualize long lists.
ListBox,ListViewandDataGridvirtualize by default throughVirtualizingStackPanel; keepVirtualizingPanel.VirtualizationMode="Recycling"so scrolled-off containers are reused instead of discarded, and avoidGroupingon very large collections, which disables virtualization. - Freeze what does not change. Call
.Freeze()onBrush,BitmapImage,Geometryand otherFreezableresources you create in code. A frozen object can be shared across threads and skips the change-notification machinery entirely. - Watch converters and bindings on hot paths. A
IValueConverterruns on every binding refresh; keep it allocation-free, and avoid multi-binding chains inside items templates that render hundreds of times. - Split templates that should not share state with
x:Shared="False"in aResourceDictionary; the default is a single shared instance per key, which causes subtle bugs when aStyleorFreezableresource is mutated at runtime. - Measure Release builds with ReadyToRun. Publish with
-p:PublishReadyToRun=truefor faster cold start; WPF does not currently support Native AOT or full trimming, because XAML loading and the binding engine depend on reflection, so do not budget for the size and startup wins Native AOT gives other .NET workloads. - Profile with the real tools.
dotnet-traceand the Visual Studio XAML analyzer surface binding errors and layout passes that Debug-mode intuition misses.
Best Practices#
- Target the current LTS (.NET 10 as of late 2026) for new projects, and budget a yearly bump for STS releases like .NET 9.
- Adopt partial-property
[ObservableProperty]over the older field-based syntax so generated members are visible to other analyzers and generators. - Keep view models free of
System.Windowstypes so they can be unit tested without an STA thread or a runningApplication. - Apply
ThemeModeat the application level rather than mixing themed and unthemed windows, which looks inconsistent to users. - Freeze and cache
Freezableresources created in code, especially brushes and geometries built per item in a list. - Keep the DI container at the composition root (
App.xaml.cs); do not let views new up services directly.
Common Pitfalls#
- Leaving
StartupUriinApp.xamlwhile also building a host. WPF then creates two windows, or the DI-resolved window never appears. - Blocking the UI thread with
.Resultor.Wait(). WPF's single-threaded apartment model makes this a common source of deadlocks; stayasyncend to end and useDispatcher.InvokeAsynconly to marshal back, not to block. - Assuming
ThemeModerecolors third-party controls. Controls from other vendors need their own Fluent-aware styles or theme packages; the built-in theme only covers stock WPF controls. - Rebuilding
CollectionViewSourcefilters on every keystroke instead of debouncing, which causes visible list flicker on large collections. - Treating
x:Shared="False"as the default. Resources are shared by default, and templates that carry mutable state need it set explicitly.
WPF vs Other .NET UI Frameworks#
| Scenario | WPF fit | Notes |
|---|---|---|
| Data-heavy Windows-only line-of-business app | Strong | Mature data binding, grids and third-party control ecosystem |
| Team wants a Windows 11 native look with less styling work | Good | Fluent theme covers most stock controls as of .NET 10 |
| App must also run on macOS, Linux or the web | Poor | WPF is Windows-only; see .NET MAUI or Blazor Hybrid |
| New Windows-only app with no legacy WPF investment | Consider WinUI 3 | Modern APIs and Fluent-by-default; see WinUI 3 and the Windows App SDK |
| Existing large WPF codebase on .NET Framework | Strong | Migration is largely mechanical; see the migration steps above |
| Startup time and package size are critical (kiosk, installer-averse users) | Weak | No Native AOT or full trimming support today |
Frequently Asked Questions#
Is WPF still actively developed in 2026?#
Yes. WPF ships a new release every year alongside .NET, with its own "what's new" notes for .NET 9, 10 and the upcoming .NET 11. Recent releases focused on the Fluent theme, performance in font rendering and XAML parsing, and bug fixes rather than new UI paradigms, which reflects WPF's role as a mature, stable framework rather than one under active redesign.
Does WPF support Native AOT or trimming?#
No, not as of .NET 10. WPF's XAML loader, style system and binding engine are built on reflection in ways that are not trim-safe today, so publishing a self-contained WPF app still ships the full runtime and libraries. If startup time and package size are hard requirements, evaluate .NET MAUI or WinUI 3, both of which support trimming, or Native AOT on some platforms.
Should a new Windows-only desktop app use WPF or WinUI 3?#
Both are supportable choices. WPF has a larger third-party control ecosystem, a more mature designer experience and more available developer knowledge, which matters for complex line-of-business UIs. WinUI 3 gives you Fluent design by default, compiled x:Bind bindings and tighter integration with Windows App SDK features. Teams with existing WPF expertise and no need for the newest Windows APIs usually get to market faster by staying on WPF.
Can I reuse CommunityToolkit.Mvvm code between WPF and .NET MAUI?#
View models, yes, almost entirely, since CommunityToolkit.Mvvm and interfaces like ICommand are UI-framework agnostic. Views are not portable: WPF XAML and MAUI XAML share a similar syntax but different namespaces, controls and layout panels, so you still write the UI layer once per platform, typically behind shared abstractions as described in Blazor Hybrid's Razor-class-library pattern or the MAUI vs WPF comparison.
Do I have to opt in to the Fluent theme?#
Yes. The default ThemeMode is None, which keeps the existing Aero2 appearance, so upgrading to .NET 9 or 10 does not change how an existing app looks. You opt in per application or per window by setting ThemeMode or merging the Fluent resource dictionary, which makes the theme safe to adopt gradually.
What changed for MVVM boilerplate with the newest CommunityToolkit.Mvvm?#
Since toolkit 8.4, [ObservableProperty] can be applied to a partial auto-property instead of a private field, using the C# 14 field keyword under the hood. The generated code is the same shape as before, but source files are shorter and IntelliSense shows the real property, not the backing field, which several analyzers previously flaged as unused.
Summary#
- WPF on modern .NET is the same mature framework, now open source and released yearly alongside .NET 8 through the upcoming .NET 11.
- CommunityToolkit.Mvvm's
[ObservableProperty]and[RelayCommand]source generators remove almost all MVVM boilerplate and work identically across WPF, WinUI 3 and MAUI. - Wire up DI with
Microsoft.Extensions.Hosting, the same generic host ASP.NET Core uses, and resolve your main window from the container instead ofStartupUri. - The Fluent theme, introduced in .NET 9 and expanded in .NET 10, is opt-in through
ThemeModeor a resource dictionary and still growing its control coverage. - WPF has no Native AOT or full trimming support; plan around that if startup time and package size are hard requirements.