Windows App Platforms: WPF, WinUI 3, WinForms, MAUI & More

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Windows has more ways to build a desktop application than any other platform, and the choice is made on day one: it decides the markup language, the packaging options, how modern the controls look and whether Native AOT is an option. This page is the entry point of the Windows Apps section. It compares the technologies that Microsoft documents, says which one the official guidance recommends for which situation, and shows the minimal project and the first view of each stack, so that the later pages can assume the choice has been made.

The page was written against these versions, current in October 2026:

  • .NET 10 LTS, latest patch 10.0.13, supported until 14 November 2028, with C# 14. .NET 8 and .NET 9 both end support on 10 November 2026.

  • .NET 11, at release candidate 2 (go-live licence). General availability is expected at .NET Conf, 10 to 13 November 2026. The WPF notes for .NET 11 list bug fixes only, so this page does not need a .NET 11 variant.

  • Visual Studio 2026, latest 18.11.1, with the ".NET desktop development" and "WinUI application development" workloads.

  • Windows App SDK 2.5.1 (stable channel). Version 2.0 was the first release to follow semantic versioning, and the NuGet package version now equals the SDK version.

  • CommunityToolkit.Mvvm 8.4.2 for the view models used in the examples.

  • Windows 11 and Windows 10. Windows 10 reached the end of support on 14 October 2025, so new work should target Windows 11 first, and the exact operating-system floor of each stack should be read from its system-requirements page.

The language itself is documented on Getting Started with C# in the C# reference. This page deals with the platforms around it. Later pages of the section go deeper into each of them: Development Environment & Tooling, Project Structure & Build, XAML Dialects: WPF, WinUI & MAUI, WinUI 3 & the Windows App SDK, Windows Forms on .NET and .NET MAUI, UWP & Other Options.

What Microsoft Recommends

The official starting point is the Windows developer FAQ and the "get started" hub on Microsoft Learn. Read together, they give this guidance, which this section follows:

  • New native Windows apps: use WinUI 3 with the Windows App SDK. It is the modern native UI framework, it ships independently of the operating system, and the Windows App SDK is where new Windows platform APIs are exposed to desktop apps.

  • WPF: a mature framework that Microsoft keeps maintaining as part of .NET. It is a sound choice for existing apps and for line-of-business tools that need its large control and tooling ecosystem. Each .NET release brings fixes and some features (the Fluent theme arrived in .NET 9).

  • Windows Forms: still receiving investment. The .NET 10 release stabilised dark mode and added asynchronous dialog and invoke APIs. It remains the fastest route to a data-entry form for existing skills and code.

  • UWP: in maintenance mode. It still works and is supported, but new features go to WinUI 3 and the Windows App SDK, and Microsoft documents migration paths away from it.

  • .NET MAUI: the cross-platform option. On Windows, MAUI runs on WinUI 3.

  • Other options: Win32 with C++, React Native for Windows and progressive web apps (PWAs) are documented as additional routes for teams with matching skills.

Two details of that guidance matter in practice. First, "recommended for new apps" is not "replace your existing apps": nothing in the official documentation says that WPF or Windows Forms apps must be rewritten. Second, the recommendation is for native apps. A team that already needs one codebase for Android, iOS and Windows is in different territory, covered further below.

The overview of the platform choices lives on the "get started" hub:

The Platform Map

The figure shows where each technology sits. Three layers matter: the UI stack the developer writes against, the frameworks and SDKs underneath (the Windows App SDK and .NET), and the operating-system APIs every stack eventually calls.

Layered map of Windows app platforms: UI stacks WinUI 3, WPF, WinForms, .NET MAUI, UWP and other routes on top; the Windows App SDK and .NET 10 in the middle; Win32, COM, DirectX and the Windows Runtime at the bottom

Three relationships in the figure explain most of the decisions that follow:

  • WPF and Windows Forms run on .NET, not on the Windows App SDK. The Windows App SDK is optional for them: a WPF or WinForms app can call its APIs (notifications, windowing helpers, app lifecycle) and can adopt MSIX packaging without changing UI framework. That is the supported way to modernise an app without rewriting it.

  • WinUI 3 is delivered by the Windows App SDK. Its controls, its XAML dialect (Microsoft.UI.Xaml) and its windowing model come from the SDK package, not from the operating system. Updating the SDK updates the controls.

  • .NET MAUI sits above WinUI 3 on Windows. A MAUI page is turned into WinUI controls by handlers, so MAUI on Windows is not an alternative renderer. It is a cross-platform abstraction over the same stack.

Win32 is at the bottom of every column. Even a WinUI 3 app asks Win32 for a window handle, and a WPF app is hosted in an HWND. This is why interop (P/Invoke, CsWin32, HwndHost) is a normal tool and not a hack, and why a later page of the section, Interop: Win32, WinForms & Web Content, covers it.

Comparison of the Stacks

The tables compare the stacks along the dimensions that decide a project: how they draw, which XAML they use, how modern they look, how they are packaged, whether Native AOT works, whether they leave Windows, and whether they are still evolving.

Technology and appearance

Aspect WPF Windows Forms WinUI 3 .NET MAUI on Windows UWP

Rendering

Retained-mode, vector based, drawn by WPF itself on top of DirectX

Wrappers around Win32 and common controls, drawn with GDI and GDI+, one window handle per control

Composition-based, GPU accelerated, part of the Windows App SDK

Handlers create WinUI 3 controls, so the rendering is that of WinUI 3

Windows Runtime XAML with the system compositor

Markup

WPF XAML (System.Windows)

None. The designer writes C# (InitializeComponent)

WinUI XAML (Microsoft.UI.Xaml)

MAUI XAML (Microsoft.Maui)

UWP XAML (Windows.UI.Xaml)

Binding

Binding with a DataContext, resolved at run time

BindingSource and Control.DataBindings

x:Bind (compiled, typed) and Binding

Binding and compiled bindings with x:DataType

x:Bind and Binding

Fluent look

Opt-in Fluent theme since .NET 9, selected with ThemeMode in .NET 10 (the code-behind API is experimental)

Classic controls. Dark mode through Application.SetColorMode, stable in .NET 10

Native Fluent controls, Mica and Acrylic backdrops

Windows controls are WinUI controls, so Fluent by default

Fluent through the WinUI 2 library, older than WinUI 3

Visual designer

XAML designer and Live Visual Tree in Visual Studio

Out-of-process Windows Forms designer

XAML designer, Hot Reload and Live Visual Tree

XAML Hot Reload

XAML designer

The practical reading: WPF gives the most expressive control over drawing, templating and animation. Windows Forms gives the shortest path from zero to a working data-entry screen. WinUI 3 gives the most current look and the newest platform integration. A WPF app can look modern with the Fluent theme, but the Fluent theme in .NET 10 is still partly experimental, and the official documentation has no deep dive for it, only the what’s new pages.

Packaging, publishing and reach

Aspect WPF Windows Forms WinUI 3 .NET MAUI on Windows UWP

Packaging

Unpackaged by default. MSIX through a packaging project, ClickOnce, or an installer

Same as WPF

Unpackaged, or packaged as MSIX (single-project MSIX)

MSIX by default, unpackaged also supported

Always packaged

Trimming and Native AOT

Not supported (build error NETSDK1168)

Not supported

PublishAot supported since Windows App SDK 1.6, documented in the release notes

Not documented for the Windows target

Compiled by the legacy .NET Native toolchain in release builds

Single-file publish

Yes (self-contained or framework-dependent)

Yes

Only unpackaged and self-contained

See the MAUI deployment pages

No

Runs outside Windows

No

No

No

Yes: Android, iOS, macOS (Mac Catalyst) and Windows

No

Language

C#, F#, Visual Basic

C#, Visual Basic

C# and C++

C#

C#, Visual Basic and C++

Support status

Maintained with .NET

Maintained with .NET, active investment

Active, stable Windows App SDK 2.5.1

Active, follows the .NET release schedule

Maintenance mode

The row about publishing deserves attention. Trimming and Native AOT are the usual answer to "make the app start faster and download smaller", and they are simply not available for WPF and Windows Forms. A team that picks WPF for its ecosystem accepts a larger self-contained output. The optimisation tools that remain (ReadyToRun, single file, framework-dependent deployment) are covered by the .NET Publishing Options page.

The same concept in three XAML dialects

WPF, WinUI 3 and MAUI all use XAML, but they are three different dialects over three different object models. The syntax is shared; the names are not.

Concept WPF WinUI 3 .NET MAUI

Default namespace

http://schemas.microsoft.com/winfx/2006/xaml/presentation

The same URI text, mapped to Microsoft.UI.Xaml

http://schemas.microsoft.com/dotnet/2021/maui

Compiled binding

Not available

x:Bind, defaults to OneTime

x:DataType with Binding

Window type

System.Windows.Window

Microsoft.UI.Xaml.Window (not a FrameworkElement)

Page hosted by Shell or NavigationPage

List control

ListView, DataGrid, ListBox

ListView, GridView, ItemsView

CollectionView

Triggers

Trigger, DataTrigger, EventTrigger

Not available, use VisualStateManager

Trigger with a limited set, and VisualStateManager

Pages later in the section go through the differences property by property. For the platform decision, the point is that markup does not move between stacks unchanged: moving from WPF to WinUI 3 is a port of the views, not a recompile.

WPF

WPF (Windows Presentation Foundation) is the oldest of the XAML stacks and the most feature-rich: a layout system, rich data binding, templates and styles, animation, 2-D and 3-D graphics, documents and printing. Since .NET Core 3.0 it has been open source and runs on modern .NET, so a WPF app on .NET 10 is not a "legacy" app in any technical sense. It runs only on Windows and only on x86, x64 and Arm64 builds of it.

Choose WPF when:

  • the app already exists, or the team already knows XAML and WPF data binding;

  • the UI is dense and custom (data grids, docking, charts, drawing surfaces), where the third-party control market is largest;

  • the app must run on the widest range of supported Windows versions;

  • a heavy investment in styles and templates should keep working.

WPF is a poor fit when the product must use the newest Windows-only controls, needs Native AOT, or must also ship to macOS, Linux or mobile.

Windows Forms

Windows Forms is the classic event-driven designer-first framework. Its controls wrap the native Win32 controls, so they look and behave like the operating system’s own, and a form is a C# class whose InitializeComponent method the designer generates. The designer runs out of process on .NET so that third-party control libraries can target .NET.

The .NET 10 release added stable dark mode support (Application.SetColorMode), the asynchronous ShowAsync, ShowDialogAsync and Control.InvokeAsync methods, clipboard changes and ScreenCaptureMode. These are the signals behind the statement that Windows Forms is "still getting investment": the framework gets modern conveniences, but its model (a retained tree of native windows, GDI based painting) does not change.

Choose Windows Forms when:

  • the app is a data-entry or utility application and productivity matters more than visual polish;

  • an existing Windows Forms app should move from .NET Framework to .NET 10 with the least change;

  • the team has Windows Forms or Visual Basic skills and no XAML skills.

WinUI 3 and the Windows App SDK

WinUI 3 is the native UI layer of the Windows App SDK, and the SDK is a set of components that ship as NuGet packages: the UI framework, windowing (AppWindow), app lifecycle (AppInstance and rich activation), app notifications, MRT Core resource management and the Windows AI APIs for Copilot+ PCs. Because the SDK ships independently from Windows, new features reach older supported Windows versions.

Release channels matter for planning. Stable is what production apps use (2.5.1 at the time of writing). Preview and Experimental channels exist for early access. Version 1.8 left maintenance on 24 September 2026, so apps still on 1.x should plan the move to 2.x.

Choose WinUI 3 when:

  • the app is new and Windows-only, and a current Fluent look, Mica and the latest Windows integration matter;

  • Native AOT is desirable (it is supported for WinUI);

  • the team wants compiled bindings (x:Bind) for typed, fast data binding.

Be aware of its constraints: it has a smaller third-party control market than WPF; single-file publishing works only for unpackaged, self-contained apps; and XAML Islands, which host WinUI controls inside WPF and Windows Forms, has no official wrapper controls and its documentation page redirects to the legacy UWP material. The WinUI 3 & the Windows App SDK page of this section covers the architecture in depth.

Source: WinUI 3

.NET MAUI on Windows

NET MAUI builds one C# and XAML project for Android, iOS, macOS (through Mac Catalyst) and Windows. On Windows the

app is a WinUI 3 app, with the MAUI handlers creating WinUI controls. Choose it when sharing code with mobile is the main requirement and a Windows-only look is secondary; do not choose it only to "use XAML on Windows", because WinUI 3 and WPF give direct access to the platform with fewer layers. A later page of the section, .NET MAUI, UWP & Other Options, covers it in more detail.

UWP: Status and Migration

The Universal Windows Platform introduced the Windows Runtime app model for Windows 10: sandboxed, always packaged, XAML in the Windows.UI.Xaml namespace, and one app that could run on PC, Xbox, HoloLens and Surface Hub. UWP is now in maintenance. What this means:

  • existing UWP apps continue to build, run and be published;

  • Microsoft fixes defects and security problems but does not add features to the UWP XAML framework;

  • new work is directed to WinUI 3 and the Windows App SDK, which reuse the same XAML ideas (x:Bind, VisualStateManager, NavigationView) and make migration a port rather than a redesign;

  • the Windows App SDK documentation includes a migration guide, and the decision guide on the migrate to Windows App SDK pages helps to decide between moving to WinUI 3 and staying where you are.

The device-family promise of UWP (Xbox, HoloLens) is the main reason an app might legitimately stay on it. For desktop-only apps there is no technical reason to start a new UWP project.

Other Routes

The Windows developer FAQ also covers technologies outside the .NET UI frameworks. They are summarised here because a team choosing a platform should know they exist, but this section does not teach them.

Route When it fits

What you give up

Win32 with C++ (and C++/WinRT for the Windows Runtime)

Maximum control over memory and start-up, game engines, system utilities and legacy code bases that are already native

Productivity: no XAML designer in the Win32 case, manual memory and lifetime management, a long learning curve

React Native for Windows

A team with React and TypeScript skills that wants to share logic with a mobile app and render native Windows controls

.NET libraries, the XAML tooling and the C# ecosystem used by the rest of this section

Progressive web app (PWA)

A web app that should be installable and feel app-like on Windows, delivered through the browser and optionally the Microsoft Store

Deep operating-system integration, and offline or native performance beyond what the web platform provides

All three can be combined with the .NET stacks. A WPF or WinUI app can host web content with WebView2, call native code through P/Invoke or C++/WinRT components, and a React Native for Windows app can bring its own native modules written in C#.

Third-party options

Two community-driven frameworks deserve a mention because teams ask about them. They are third-party projects, not Microsoft products, and the official documentation does not cover them, so no tutorial is given here.

  • Avalonia is an open-source XAML framework that draws its own controls and runs on Windows, macOS, Linux, mobile and the browser. Its XAML is modelled on WPF, which makes it the usual choice for taking a WPF code base to Linux or macOS.

  • Uno Platform is an open-source framework that implements the WinUI API surface on other platforms, so WinUI code can run on iOS, Android, macOS, Linux and WebAssembly.

Both depend on a vendor or community that is not Microsoft, so evaluate their licence, support and release cadence before committing. Test tools such as FlaUI (UI automation) and installers such as Velopack are likewise third-party and are mentioned on the pages where they matter.

Choosing a Stack

A decision flow

The flow starts from the situation rather than from the technology. It encodes the guidance above, so it is a starting point and not a verdict.

flowchart TD A["Start: what is the situation?"] --> B{"Existing app?"} B -->|"Yes"| C{"Framework today"} B -->|"No, new app"| N{"Must it run on Android, iOS or macOS too?"} C -->|"WPF or WinForms on .NET Framework"| M1["Migrate to .NET 10 first - keep the UI stack"] C -->|"WPF or WinForms on .NET 8 or 9"| M2["Move to .NET 10 before 10 Nov 2026 - keep the UI stack"] C -->|"UWP"| M3["Stay in maintenance, or port to WinUI 3 - see the migration decision guide"] M1 --> M4["Optionally add Windows App SDK features - or package as MSIX"] M2 --> M4 N -->|"Yes"| X1{"C# and XAML skills?"} N -->|"No, Windows only"| W1{"Need Native AOT or newest Fluent look?"} X1 -->|"Yes"| X2[".NET MAUI"] X1 -->|"No, web skills"| X3["React Native for Windows or a PWA"] W1 -->|"Yes"| W2["WinUI 3 and Windows App SDK"] W1 -->|"No"| W3{"Dense custom UI or rich controls?"} W3 -->|"Yes"| W4["WPF"] W3 -->|"No, forms over data"| W5["Windows Forms or WinUI 3"]

New apps

For a new Windows-only desktop app, start from WinUI 3 unless a concrete requirement points elsewhere. The requirements that justify another stack are specific and checkable:

Requirement Prefer Reason

A control suite or a design tool that exists only for WPF

WPF

The ecosystem is larger, and rewriting the controls costs more than living with the older look

A forms-over-data tool built by a small team in days

Windows Forms

The designer and BindingSource give the shortest path, and the output is a normal .NET 10 app

Android, iOS and Windows from one team and codebase

.NET MAUI

One project, one language, and the Windows target is WinUI 3

Native AOT, smallest start-up, newest Windows APIs

WinUI 3

It is the only XAML stack with PublishAot

A web team that wants a desktop presence

PWA or React Native for Windows

Reuses existing skills and code

Existing apps

For an existing app the cost of change dominates the decision, and the official guidance is conservative:

  1. Move to modern .NET first. A WPF or Windows Forms app on .NET Framework 4.x should move to .NET 10 LTS. This is the step that brings support, performance and the new APIs, and it does not change the UI framework.

  2. Add Windows App SDK features without changing the UI. Packaging with MSIX, app notifications, windowing helpers and the Windows AI APIs can be used from WPF and Windows Forms.

  3. Port the UI only when a requirement demands it. Moving to WinUI 3 is justified by Native AOT, the Fluent look or Windows features that cannot be reached from the old stack. It is a port of views and bindings.

  4. Keep UWP apps on UWP unless there is a feature to gain, because maintenance mode does not mean end of life.

Minimal Projects for Each Stack

The commands and project files below create the smallest app of each stack on .NET 10. They use the SDK-style project format: one .csproj, no packages.config, and the target framework moniker (TFM) decides which Windows APIs are visible.

Target framework monikers

Stack Target framework Notes

WPF

net10.0-windows

UseWPF is true. A version suffix such as net10.0-windows10.0.19041.0 is optional and only needed to call Windows Runtime APIs

Windows Forms

net10.0-windows

UseWindowsForms is true

WinUI 3

net10.0-windows10.0.19041.0

UseWinUI is true and a Windows SDK version is required, because WinUI uses Windows Runtime APIs

Windows Forms or WPF library

net10.0-windows

OutputType stays Library, and UseWPF or UseWindowsForms is still needed to reference the framework

Shared view model library

net10.0

No Windows dependency, so it can be unit-tested anywhere and reused by every UI project

The -windows suffix is not a detail. A project targeting net10.0 cannot use WPF or Windows Forms types, and the build fails with a missing assembly error if UseWPF or UseWindowsForms is absent.

Creating the projects

The first commands create the solution used by all the later examples: a shared library and one UI project per stack.

# Check the SDK: 10.0.x with x >= 13 at the time of writing
dotnet --version

# Solution and shared code (no Windows dependency)
dotnet new sln -n BookshelfDesktop
dotnet new classlib -n Bookshelf.Core -f net10.0
dotnet new wpf -n Bookshelf.Wpf -f net10.0
dotnet new winforms -n Bookshelf.WinForms -f net10.0

dotnet sln add Bookshelf.Core Bookshelf.Wpf Bookshelf.WinForms
dotnet add Bookshelf.Core package CommunityToolkit.Mvvm --version 8.4.2
dotnet add Bookshelf.Wpf reference Bookshelf.Core
dotnet add Bookshelf.WinForms reference Bookshelf.Core

# See which WinUI templates are installed or available
dotnet new list winui
dotnet new search winui

The WPF and Windows Forms templates ship with the .NET SDK. WinUI project templates ship with the "WinUI application development" Visual Studio workload, and the dotnet new search winui command shows whether template packs for the command line are available on the machine. The recommended way to set the workstation up, including the workloads and Developer Mode, is the "start here" page and the configuration file it points to; the Development Environment & Tooling page of this section covers it.

The WPF project file

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
    <RootNamespace>Bookshelf.Wpf</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\Bookshelf.Core\Bookshelf.Core.csproj" />
  </ItemGroup>

</Project>

The Windows Forms project file

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWindowsForms>true</UseWindowsForms>
    <RootNamespace>Bookshelf.WinForms</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\Bookshelf.Core\Bookshelf.Core.csproj" />
  </ItemGroup>

</Project>

The WinUI 3 project file

The WinUI project uses the Windows App SDK package, a TFM with a Windows SDK version and the platform list that the Visual Studio templates generate. WindowsPackageType set to None produces an unpackaged app that runs from the build output; removing that line and keeping EnableMsixTooling gives the single-project MSIX layout described on the MSIX Packaging & Signing page.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
    <RootNamespace>Bookshelf.WinUI</RootNamespace>
    <Platforms>x86;x64;ARM64</Platforms>
    <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
    <UseWinUI>true</UseWinUI>
    <EnableMsixTooling>true</EnableMsixTooling>
    <WindowsPackageType>None</WindowsPackageType>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="2.5.1" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Bookshelf.Core\Bookshelf.Core.csproj" />
  </ItemGroup>

</Project>

What the three project files have in common

  • OutputType is WinExe so that no console window appears.

  • Nullable and ImplicitUsings follow the .NET 10 defaults for new projects, and the section’s code samples assume them.

  • The UI framework is switched on by one MSBuild property (UseWPF, UseWindowsForms or UseWinUI); the SDK then adds the references, the XAML compiler (for WPF and WinUI) and the generated InitializeComponent plumbing.

  • A ProjectReference to the shared library keeps view models and services free of UI types.

One View Model, Three Views

The Bookshelf Desktop scenario is the running example of the section. The view model belongs to the shared library and is written once, so the three stacks can be compared on the same behaviour: load the books, filter by text, and delete the selected book after a confirmation.

The shared library

The shared library project is called Bookshelf.Core and holds every type below; the namespaces follow the canonical Bookshelf Desktop layout of the section (Bookshelf.Core for Book and IBookRepository, Bookshelf.Desktop for IDialogService, Bookshelf.Desktop.ViewModels for the view models). The repository is an in-memory stand-in for the SQLite repository of the Local Data, Settings & Files page.

// Bookshelf.Core/Book.cs
namespace Bookshelf.Core;

public sealed record Book(int Id, string Title, string Author, int Year = 0, decimal Price = 0m, string? Isbn = null);

// Bookshelf.Core/IBookRepository.cs
namespace Bookshelf.Core;

public interface IBookRepository
{
    Task<IReadOnlyList<Book>> GetAllAsync(CancellationToken cancellationToken);
    Task<Book?> GetByIdAsync(int id, CancellationToken cancellationToken);
    Task<Book> AddAsync(Book book, CancellationToken cancellationToken);
    Task UpdateAsync(Book book, CancellationToken cancellationToken);
    Task DeleteAsync(int id, CancellationToken cancellationToken);
}

// Bookshelf.Core/IDialogService.cs
namespace Bookshelf.Desktop;

using Bookshelf.Core;

public interface IDialogService
{
    Task<bool> ConfirmAsync(string title, string message);

    /// <summary>Shows the add/edit dialog. Returns null when the user cancels.</summary>
    Task<Book?> EditBookAsync(Book? existing);
}

// Bookshelf.Core/InMemoryBookRepository.cs
namespace Bookshelf.Core;

public sealed class InMemoryBookRepository : IBookRepository
{
    private readonly List<Book> _books =
    [
        new(1, "The Pragmatic Programmer", "Andrew Hunt", 1999),
        new(2, "Refactoring", "Martin Fowler", 1999),
        new(3, "Domain-Driven Design", "Eric Evans", 2003),
    ];

    public Task<IReadOnlyList<Book>> GetAllAsync(CancellationToken cancellationToken)
        => Task.FromResult<IReadOnlyList<Book>>(_books.ToList());

    public Task<Book?> GetByIdAsync(int id, CancellationToken cancellationToken)
        => Task.FromResult(_books.FirstOrDefault(book => book.Id == id));

    public Task<Book> AddAsync(Book book, CancellationToken cancellationToken)
    {
        var added = book with { Id = _books.Count == 0 ? 1 : _books.Max(b => b.Id) + 1 };
        _books.Add(added);
        return Task.FromResult(added);
    }

    public Task UpdateAsync(Book book, CancellationToken cancellationToken)
    {
        var index = _books.FindIndex(b => b.Id == book.Id);
        if (index >= 0)
        {
            _books[index] = book;
        }

        return Task.CompletedTask;
    }

    public Task DeleteAsync(int id, CancellationToken cancellationToken)
    {
        _books.RemoveAll(book => book.Id == id);
        return Task.CompletedTask;
    }
}

Source: MVVM Toolkit

// Bookshelf.Core/BooksViewModel.cs
using System.Collections.ObjectModel;
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace Bookshelf.Desktop.ViewModels;

public partial class BookViewModel(Book book) : ObservableObject
{
    public Book Model { get; } = book;

    public int Id => Model.Id;

    public string Title => Model.Title;

    public string Author => Model.Author;

    public int Year => Model.Year;

    [ObservableProperty]
    public partial bool IsFavorite { get; set; }
}

public partial class BooksViewModel(IBookRepository repository, IDialogService dialogs) : ObservableObject
{
    private List<BookViewModel> _all = [];

    public ObservableCollection<BookViewModel> Books { get; } = [];

    [ObservableProperty]
    public partial string? SearchText { get; set; }

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(DeleteSelectedCommand))]
    public partial BookViewModel? SelectedBook { get; set; }

    partial void OnSearchTextChanged(string? value) => ApplyFilter();

    [RelayCommand(IncludeCancelCommand = true)]
    private async Task LoadAsync(CancellationToken cancellationToken)
    {
        var books = await repository.GetAllAsync(cancellationToken);
        _all = books.Select(book => new BookViewModel(book)).ToList();
        ApplyFilter();
    }

    [RelayCommand(CanExecute = nameof(CanDeleteSelected))]
    private async Task DeleteSelectedAsync()
    {
        if (SelectedBook is not { } book)
        {
            return;
        }

        if (!await dialogs.ConfirmAsync("Delete book", $"Delete \"{book.Title}\"?"))
        {
            return;
        }

        await repository.DeleteAsync(book.Id, CancellationToken.None);
        _all.Remove(book);
        ApplyFilter();
    }

    private bool CanDeleteSelected() => SelectedBook is not null;

    private void ApplyFilter()
    {
        var text = SearchText?.Trim();
        var matches = string.IsNullOrEmpty(text)
            ? _all
            : _all.Where(b => b.Title.Contains(text, StringComparison.CurrentCultureIgnoreCase)
                           || b.Author.Contains(text, StringComparison.CurrentCultureIgnoreCase)).ToList();

        Books.Clear();
        foreach (var book in matches)
        {
            Books.Add(book);
        }
    }
}

The partial-property form of [ObservableProperty] needs C# 14 and the MVVM Toolkit 8.4.1 or later, both of which the baseline of this section satisfies. The toolkit documentation pages still show the field form of the attribute, and the page about the MVVMTK0042 analyzer still mentions LangVersion preview, so the release notes are the source for the partial-property syntax. There is no Microsoft Learn MVVM article written specifically for WPF either; the guidance is spread over the toolkit documentation, the .NET MAUI enterprise patterns book and the WinUI data binding pages. The same view model works unchanged in all three stacks below.

WPF: DataContext and Binding

<!-- Bookshelf.Wpf/MainWindow.xaml -->
<Window x:Class="Bookshelf.Wpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Bookshelf Desktop" Height="420" Width="680">
    <Grid Margin="12" RowDefinitions="Auto, *, Auto">
        <TextBox Grid.Row="0"
                 Margin="0,0,0,8"
                 AutomationProperties.Name="Search books"
                 Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />

        <DataGrid Grid.Row="1"
                  AutoGenerateColumns="False"
                  IsReadOnly="True"
                  ItemsSource="{Binding Books}"
                  SelectedItem="{Binding SelectedBook}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Title" Binding="{Binding Title}" Width="2*" />
                <DataGridTextColumn Header="Author" Binding="{Binding Author}" Width="*" />
                <DataGridTextColumn Header="Year" Binding="{Binding Year}" />
            </DataGrid.Columns>
        </DataGrid>

        <Button Grid.Row="2"
                Margin="0,8,0,0"
                HorizontalAlignment="Right"
                Content="Delete"
                Command="{Binding DeleteSelectedCommand}" />
    </Grid>
</Window>
// Bookshelf.Wpf/MainWindow.xaml.cs
using Bookshelf.Desktop.ViewModels;

namespace Bookshelf.Wpf;

public partial class MainWindow : System.Windows.Window
{
    public MainWindow(BooksViewModel viewModel)
    {
        InitializeComponent();
        DataContext = viewModel;
        Loaded += async (_, _) => await viewModel.LoadCommand.ExecuteAsync(null);
    }
}

// Bookshelf.Wpf/WpfDialogService.cs
using Bookshelf.Core;
using Bookshelf.Desktop;

namespace Bookshelf.Wpf;

public sealed class WpfDialogService : IDialogService
{
    public Task<bool> ConfirmAsync(string title, string message)
    {
        var result = System.Windows.MessageBox.Show(
            message, title, System.Windows.MessageBoxButton.YesNo, System.Windows.MessageBoxImage.Question);
        return Task.FromResult(result == System.Windows.MessageBoxResult.Yes);
    }

    // The add/edit dialog is shown on the Windows, Dialogs & Navigation page; this comparison has no edit screen.
    public Task<Book?> EditBookAsync(Book? existing) => Task.FromResult<Book?>(null);
}

// Bookshelf.Wpf/App.xaml.cs  (remove StartupUri from App.xaml)
using Bookshelf.Core;
using Bookshelf.Desktop.ViewModels;

namespace Bookshelf.Wpf;

public partial class App : System.Windows.Application
{
    protected override void OnStartup(System.Windows.StartupEventArgs e)
    {
        base.OnStartup(e);
        var viewModel = new BooksViewModel(new InMemoryBookRepository(), new WpfDialogService());
        new MainWindow(viewModel).Show();
    }
}

The Grid attribute RowDefinitions="Auto, *, Auto" is the .NET 10 short form of a RowDefinitions collection; on earlier versions the element form is required. The constructor creates the objects by hand to keep the example short. The Dependency Injection & the Generic Host page replaces that with the Generic Host, and the official documentation provides no WPF dialog service, so IDialogService is a user pattern, not an API.

WinUI 3: x:Bind

<!-- Bookshelf.WinUI/MainWindow.xaml -->
<Window
    x:Class="Bookshelf.WinUI.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vm="using:Bookshelf.Desktop.ViewModels">

    <Grid x:Name="RootGrid" Padding="12" RowSpacing="8">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBox Grid.Row="0"
                 AutomationProperties.Name="Search books"
                 Text="{x:Bind ViewModel.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

        <ListView Grid.Row="1"
                  ItemsSource="{x:Bind ViewModel.Books}"
                  SelectedItem="{x:Bind ViewModel.SelectedBook, Mode=TwoWay}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="vm:BookViewModel">
                    <StackPanel Padding="0,4">
                        <TextBlock Text="{x:Bind Title}" Style="{StaticResource BodyStrongTextBlockStyle}" />
                        <TextBlock Text="{x:Bind Author}" />
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <Button Grid.Row="2"
                HorizontalAlignment="Right"
                Content="Delete"
                Command="{x:Bind ViewModel.DeleteSelectedCommand}" />
    </Grid>
</Window>
// Bookshelf.WinUI/MainWindow.xaml.cs
using Bookshelf.Desktop.ViewModels;
using Microsoft.UI.Xaml;

namespace Bookshelf.WinUI;

public sealed partial class MainWindow : Window
{
    public MainWindow(BooksViewModel viewModel)
    {
        ViewModel = viewModel;
        InitializeComponent();
        RootGrid.Loaded += async (_, _) => await ViewModel.LoadCommand.ExecuteAsync(null);
    }

    public BooksViewModel ViewModel { get; }
}

The WinUI version differs in three visible ways. Bindings use x:Bind and are resolved against the window class (so ViewModel is a property of the window, and no DataContext is involved). The DataTemplate declares its data type with x:DataType. Window is not a FrameworkElement, so there is no Loaded event or DataContext on it, and the example uses a named root element. The Grid has no short row-definition syntax in WinUI. A dialog service for WinUI needs a ContentDialog and an XamlRoot taken from the window content, which is why it is not shown here.

Windows Forms: BindingSource

// Bookshelf.WinForms/MainForm.cs
using System.Collections.Specialized;
using Bookshelf.Desktop.ViewModels;

namespace Bookshelf.WinForms;

public sealed class MainForm : Form
{
    private readonly BooksViewModel _viewModel;
    private readonly BindingSource _books = new();
    private readonly TextBox _search = new() { Dock = DockStyle.Top };
    private readonly DataGridView _grid = new()
    {
        Dock = DockStyle.Fill,
        ReadOnly = true,
        AllowUserToAddRows = false,
        SelectionMode = DataGridViewSelectionMode.FullRowSelect,
        MultiSelect = false,
    };
    private readonly Button _delete = new() { Text = "Delete", Dock = DockStyle.Bottom };

    public MainForm(BooksViewModel viewModel)
    {
        _viewModel = viewModel;
        Text = "Bookshelf Desktop";
        ClientSize = new Size(680, 420);
        AccessibleName = "Bookshelf Desktop";
        _search.AccessibleName = "Search books";

        Controls.Add(_grid);
        Controls.Add(_search);
        Controls.Add(_delete);

        // BookViewModel has more public properties than the grid should show, so declare the columns.
        _grid.AutoGenerateColumns = false;
        _grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(BookViewModel.Title), HeaderText = "Title" });
        _grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(BookViewModel.Author), HeaderText = "Author" });
        _grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(BookViewModel.Year), HeaderText = "Year" });

        _books.DataSource = viewModel.Books;
        _grid.DataSource = _books;
        _search.DataBindings.Add(nameof(TextBox.Text), viewModel, nameof(BooksViewModel.SearchText),
            false, DataSourceUpdateMode.OnPropertyChanged);
        _delete.Command = viewModel.DeleteSelectedCommand;

        // ObservableCollection does not implement IBindingList, so refresh the BindingSource by hand
        viewModel.Books.CollectionChanged += OnBooksChanged;
        _grid.SelectionChanged += (_, _) => viewModel.SelectedBook = _books.Current as BookViewModel;
    }

    protected override async void OnShown(EventArgs e)
    {
        base.OnShown(e);
        await _viewModel.LoadCommand.ExecuteAsync(null);
    }

    private void OnBooksChanged(object? sender, NotifyCollectionChangedEventArgs e)
        => _books.ResetBindings(false);
}

// Bookshelf.WinForms/Program.cs
using Bookshelf.Core;
using Bookshelf.Desktop.ViewModels;
using Bookshelf.WinForms;

internal static class Program
{
    [STAThread]
    private static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.Run(new MainForm(new BooksViewModel(new InMemoryBookRepository(), new MessageBoxDialogService())));
    }
}

// Bookshelf.WinForms/MessageBoxDialogService.cs
using Bookshelf.Core;
using Bookshelf.Desktop;

namespace Bookshelf.WinForms;

internal sealed class MessageBoxDialogService : IDialogService
{
    public Task<bool> ConfirmAsync(string title, string message)
        => Task.FromResult(MessageBox.Show(message, title, MessageBoxButtons.YesNo, MessageBoxIcon.Question)
                           == DialogResult.Yes);

    public Task<Book?> EditBookAsync(Book? existing) => Task.FromResult<Book?>(null);
}

Windows Forms binds through BindingSource and Control.DataBindings. It has no XAML, so the form is written in code here to keep the example complete; in a real project the designer produces the same assignments in MainForm.Designer.cs. The Button.Command property, which lets a button invoke an ICommand and follow its CanExecute state, was added in .NET 7 and is what makes the MVVM Toolkit commands usable without an event handler. The ObservableCollection<T> type does not implement IBindingList, so the form refreshes the BindingSource itself when the collection changes. It is one of the small gaps that make MVVM in Windows Forms feel less natural than in the XAML stacks. The Windows Forms on .NET page of this section goes deeper.

From .NET Framework to .NET 8, 9 and 10

Most existing WPF and Windows Forms apps run on .NET Framework 4.x. The table lists what changes when they move to modern .NET, and which release brought which change. It is a summary: the Migrating from .NET Framework page of the section follows the migration step by step.

Area Change

Release

Runtime

.NET Framework stays installed with Windows and receives only servicing. Modern .NET is side-by-side, versioned and shipped with the app or installed as the Desktop Runtime

.NET Core 3.0 first ran WPF and Windows Forms

Project format

SDK-style projects with UseWPF or UseWindowsForms, no packages.config, and an explicit -windows TFM

.NET Core 3.0 onwards

Removed technologies

AppDomains for isolation, .NET Remoting, Code Access Security and WCF server hosting are unavailable. WebBrowser is replaced by WebView2

Differences from .NET Framework

Serialization

BinaryFormatter is removed, which affects clipboard and drag-and-drop of custom objects in Windows Forms and WPF

.NET 9; clipboard guidance for .NET 10

WPF Fluent theme

An opt-in Fluent theme, with accent colour support

.NET 9

WPF in .NET 10

ThemeMode selection, the short Grid definition syntax, new MessageBox buttons and results, and performance work

.NET 10

Windows Forms in .NET 10

Stable dark mode (Application.SetColorMode), ShowAsync, ShowDialogAsync, Control.InvokeAsync, clipboard API changes

.NET 10

Trimming and Native AOT

Not available, even on modern .NET. The build reports NETSDK1168 for WPF

All versions

Support

.NET 8 and .NET 9 end on 10 November 2026; .NET 10 is supported until 14 November 2028

.NET 10 is the LTS baseline

The .NET Upgrade Assistant, which older articles recommend, is deprecated. Microsoft now points to GitHub Copilot app modernization for assisted upgrades. The WPF documentation additionally has a migration section with a manual walkthrough and a "differences from .NET Framework" page.

Modernising Without a Rewrite

The Windows App SDK documentation describes a path between "keep as is" and "rewrite in WinUI 3": add Windows App SDK features to a WPF or Windows Forms app. The usual order is packaging first (an MSIX package gives the app identity, clean install and uninstall, and access to features that require identity), then notifications and lifecycle, then selected WinUI controls through XAML Islands where the lack of wrapper controls is acceptable. The Windows Platform Integration and MSIX Packaging & Signing pages follow it.

Pitfalls

  • Choosing by novelty. "Recommended for new apps" is not a deprecation of WPF or Windows Forms. A rewrite to WinUI 3 costs the views, the styles, the third-party controls and the testing, and should be justified by a requirement.

  • Forgetting the -windows TFM. net10.0 projects cannot use WPF or Windows Forms. WinUI needs the Windows SDK version in the TFM (net10.0-windows10.0.19041.0 in the examples).

  • Expecting Native AOT everywhere. AOT works for WinUI and not for WPF or Windows Forms. Plan start-up optimisation with ReadyToRun instead.

  • Expecting identical XAML. A WPF Trigger has no WinUI equivalent, x:Bind does not exist in WPF, and Window has different base types. Plan a port as work, not a rename.

  • Mixing up the SDK and the framework. The Windows App SDK is not .NET, and updating it does not change the .NET runtime. A WinUI project has two version numbers to keep consistent.

  • Staying on end-of-support runtimes. .NET 8 and .NET 9 end support on 10 November 2026, so any new project should start on .NET 10 and existing ones should plan the update.

  • Assuming a rewrite is the only way to get modern features. Packaging, notifications and windowing helpers reach WPF and Windows Forms through the Windows App SDK.

  • Trusting year-old tutorials. The Upgrade Assistant, the App Center crash reporting service and the old WinUI 3 preview templates have been replaced or retired.

Performance and Security Considerations

On performance, the stack decides the baseline. WPF start-up is dominated by JIT time and assembly loading, so ReadyToRun and a lean App.xaml matter. WinUI 3 apps can use Native AOT for the smallest start-up. Windows Forms is light, but heavy custom painting runs on the GDI path. In every stack the rule is the same: keep the UI thread free, load data asynchronously and virtualise long lists. The Performance & Diagnostics page covers measurement.

On security, the platform affects the options available. MSIX packages run with a defined identity and declared capabilities and are signed, which gives the user a clear install and uninstall story; unpackaged apps have no container and need their own signing and update security. Code signing and SmartScreen reputation apply to all stacks. The Security & App Isolation page covers the details. Loading XAML at run time from untrusted sources is unsafe in WPF, because XAML can instantiate arbitrary types.

What the Official Documentation Does Not Provide

The Microsoft documentation is detailed, but it has gaps that affect this page’s topic, and this section does not fill them with invented material:

  • There is no single, authoritative feature matrix of WPF, Windows Forms, WinUI 3 and MAUI. The Windows developer FAQ gives recommendations in prose; the tables on this page are an editorial summary built from the individual framework pages.

  • Native AOT support for WinUI is documented in release notes, not in a conceptual page, and Native AOT for WPF and Windows Forms is simply stated as unsupported.

  • XAML Islands hosting WinUI 3 in WPF or Windows Forms has no wrapper controls, and its documentation link redirects to the legacy UWP page.

  • There is no official dialog service, navigation service or ViewModel locator for WPF or Windows Forms, and no Learn article about MVVM written for WPF. The IDialogService in the examples is a pattern, not an API.

  • There is no conceptual page on the WPF Ribbon (API reference only), no deep dive on Fluent theming in WPF, and no current Microsoft recommendation for desktop UI test automation.

  • Third-party frameworks (Avalonia, Uno Platform) and tools (FlaUI, Velopack) are outside the official documentation. The section mentions them by name and does not teach them.

Where to Go Next

The Development Environment & Tooling page sets up Visual Studio 2026, Developer Mode and the command-line tools. Project Structure & Build explains the files created above. For the XAML-based stacks, XAML Language Fundamentals and XAML Dialects: WPF, WinUI & MAUI explain the markup. Publishing starts with .NET Publishing Options. Related topics are tracked outside this page: the Visual Basic section covers Windows Forms and WPF in VB, and the CLI apps section covers console applications, which are not UI stacks.