Interop: Win32, WinForms & Web Content

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.

Real Windows applications are rarely pure. A WPF app may need an old ActiveX-era grid from a vendor, a Windows Forms designer surface, a native video control, an embedded web page, or one Win32 call that the framework never wrapped. WPF, Windows Forms and WinUI 3 are three different UI stacks, but they all sit on the same operating system primitive: the window handle (HWND). Interop is the art of putting one stack’s HWND inside another’s, and of reaching across to the operating system directly. This page covers the hosting classes, the one rule that constrains all of them (airspace), the web-content options for Bookshelf Desktop, native calls, and what is and is not available for hosting WinUI 3 controls inside WPF or Windows Forms.

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

  • .NET 10 LTS, latest patch 10.0.13, with C# 14. .NET 11 is at release candidate 2 (general availability is expected 10 to 13 November 2026). The hosting classes in System.Windows.Interop and System.Windows.Forms.Integration are unchanged across .NET Framework 4.8 and .NET 8, 9 and 10.

  • Windows App SDK 2.5.1 (stable) for the XAML Islands section.

  • Visual Studio 2026 18.11.1.

  • The Microsoft.Web.WebView2 NuGet package and the WebView2 Runtime for the web-content section. The package version is not pinned here; take the current stable one from NuGet.

Choosing an Interop Technique

Need Technique Direction

Host a native or custom Win32 window inside WPF

HwndHost (derive from it)

Win32 inside WPF

Host WPF content inside a native Win32 window

HwndSource

WPF inside Win32

Observe or intercept window messages of a WPF window

HwndSource.AddHook

Win32 messages into WPF

Reuse a Windows Forms control inside WPF

WindowsFormsHost

WinForms inside WPF

Reuse a WPF control inside a Windows Forms form

ElementHost

WPF inside WinForms

Show web content (HTML, JavaScript)

WebView2 control for WPF or WinForms

Chromium inside either

Reuse Razor components as UI

BlazorWebView

Blazor inside WPF or WinForms

Call an operating system function

P/Invoke, ideally generated by CsWin32

Managed to native

Call a Windows Runtime or Windows App SDK API

Windows-specific target framework moniker

Managed to WinRT

Show WinUI 3 controls inside a WPF or WinForms window

WinUI XAML Islands (DesktopWindowXamlSource)

WinUI inside either

The techniques combine freely at one level: a WPF window can hold a WindowsFormsHost, an HwndHost and a WebView2 side by side. Nesting is the limit. Microsoft Learn states that multilevel hybrid controls (for example a Windows Forms control that contains a WPF control that contains another Windows Forms control) are not supported, so treat one level of nesting as the maximum.

How WPF Uses Window Handles

Understanding interop starts with an architectural fact from the WPF interop documentation: all WPF content on the screen is ultimately backed by an HWND. When you create a WPF Window, WPF creates one top-level HWND and uses an HwndSource to put the Window and all its WPF content inside it. The rest of the WPF elements share that one handle. The exceptions are menus, combo box drop-downs and other pop-ups, each of which creates its own top-level window, which is why a WPF menu can extend past the edge of the window that opened it.

This is the reverse of Windows Forms, where almost every control is its own HWND. It explains both why WPF rendering is flexible (transforms, opacity and clipping happen inside one surface) and why embedding another technology is restricted: the embedded child is a real child HWND that the Windows window manager composes, not something WPF can draw.

The two main classes are:

  • HwndSource wraps WPF content in an HWND. Set its RootVisual to a WPF element, give it a parent HWND, and the content becomes a child window of any Win32 window. Its Handle property is the HWND. WPF’s own Window uses it internally.

  • HwndHost is a FrameworkElement (not a Control) that wraps an HWND so it can be placed in a WPF element tree. You derive from it and override BuildWindowCore (create and return the child window), DestroyWindowCore (destroy it) and optionally WndProc (handle its messages). WPF tells Win32 where to position the child relative to the WPF window.

Airspace

Interop has one rule that surprises everybody once. Within a top-level window each pixel belongs to exactly one HWND, and therefore to exactly one rendering technology. Microsoft Learn calls the set of pixels an HWND owns its region, and the informal name is airspace. Everything that renders above a pixel during the lifetime of the application must be part of the same rendering technology. Rendering WPF pixels over a Win32 region is "disallowed as much as possible through the interoperation APIs", and mixing transparency between technologies cannot be built because a semi-transparent pixel would be owned by two renderers.

Airspace: a window divided into WPF and child HWND regions, and the rules a hosted HWND imposes

The practical consequences, all from the interop documentation:

Aspect Behaviour of a hosted HWND (HwndHost, WindowsFormsHost, WebView2 WPF control)

Z-order

Always drawn on top of other WPF elements in the same top-level window

Transforms

HwndHost cannot be rotated, scaled or skewed. A hosted Windows Forms control keeps its upright position

Opacity

Only Opacity of 1.0 is supported. Alpha blending of the host is not possible

FlowDirection and similar properties

Setting them on the host has no effect; they are not mapped to Win32

Clipping

The host ignores the clipping region of its parent, which matters inside a ScrollViewer or Canvas

Pop-ups

ToolTip and ContextMenu are separate top-level windows, so they still appear correctly over the host

Mouse input

While the pointer is over the host, WPF mouse events are not raised and IsMouseOver is false

Keyboard input

While the host has focus, WPF keyboard events are not raised and IsKeyboardFocusWithin is false

Focus events

GotFocus and LostFocus are not raised for focus changes inside the host

Designing around airspace is a layout decision, not a code fix: put toolbars, overlays and busy indicators beside the hosted region instead of over it; show modal dialogs and notifications as separate windows; and do not animate or fade a host. Tab and mnemonic handling across the boundary is the job of IKeyboardInputSink and IKeyboardInputSite, which HwndHost and HwndSource implement; override the appropriate methods when the default behaviour is not enough, and note that inside the Win32 region the tabbing logic belongs entirely to the Win32 code.

Hosting a Win32 Window in WPF with HwndHost

The recipe from the documentation is: derive from HwndHost, create the child window in BuildWindowCore, destroy it in DestroyWindowCore, then add an instance to the WPF tree. The following example hosts the system EDIT control as a native notes box for Bookshelf Desktop, using the source-generated LibraryImport form of P/Invoke (see the Native Interop page of the C# reference for the details of that attribute). The source generator emits unsafe code, so the project needs <AllowUnsafeBlocks>true</AllowUnsafeBlocks>; without it the build fails with SYSLIB1062.

using System.Runtime.InteropServices;
using System.Windows.Interop;

namespace Bookshelf.Desktop.Interop;

/// <summary>Hosts a native single-line EDIT control inside a WPF element tree.</summary>
public sealed partial class NativeEditHost : HwndHost
{
    private const int WS_CHILD = 0x40000000;
    private const int WS_VISIBLE = 0x10000000;
    private const int WS_BORDER = 0x00800000;

    private nint _hwnd;

    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        _hwnd = CreateWindowEx(
            0,
            "EDIT",
            string.Empty,
            WS_CHILD | WS_VISIBLE | WS_BORDER,
            0, 0, (int)Width, (int)Height,
            hwndParent.Handle,
            nint.Zero,
            nint.Zero,
            nint.Zero);

        return new HandleRef(this, _hwnd);
    }

    protected override void DestroyWindowCore(HandleRef hwnd)
    {
        DestroyWindow(hwnd.Handle);
        _hwnd = nint.Zero;
    }

    [LibraryImport("user32.dll", EntryPoint = "CreateWindowExW", StringMarshalling = StringMarshalling.Utf16)]
    private static partial nint CreateWindowEx(
        int exStyle, string className, string windowName, int style,
        int x, int y, int width, int height,
        nint parent, nint menu, nint instance, nint param);

    [LibraryImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static partial bool DestroyWindow(nint hwnd);
}

Using it from XAML needs only an element with an explicit size, because the host reports the size you give it:

<Window x:Class="Bookshelf.Desktop.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:interop="clr-namespace:Bookshelf.Desktop.Interop"
        Title="Bookshelf Desktop" Height="320" Width="480">
    <StackPanel Margin="12">
        <TextBlock Text="Native notes (an HWND, so it ignores WPF styling)" Margin="0,0,0,6"/>
        <interop:NativeEditHost Width="320" Height="24" HorizontalAlignment="Left"/>
    </StackPanel>
</Window>

Source: WPF and Win32 interop; the example is a minimal illustration of the documented BuildWindowCore and DestroyWindowCore pattern, not an official sample (the official walkthrough hosts a list box).

Messages from the hosted window reach you in two equivalent ways, per the documentation: override HwndHost.WndProc, or handle the MessageHook event on the host. You cannot process messages from windows in another process with WndProc. To send a message to the hosted window (for example WM_SETTEXT), P/Invoke SendMessage.

Hosting WPF in a Win32 Window with HwndSource

The reverse direction wraps a WPF element tree in a child window of a Win32 parent. The documentation steps are: create an HwndSource with the parent HWND, create the WPF content, assign it to RootVisual, and use Handle in the native code. Keep a reference to the HwndSource, or it can be garbage collected. In a managed Windows Forms or Win32 interop shell this looks like:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;

namespace Bookshelf.Desktop.Interop;

public static class WpfIsland
{
    private static HwndSource? s_source; // keeps the source alive

    public static nint Attach(nint parentHwnd, int width, int height)
    {
        var parameters = new HwndSourceParameters("BookshelfIsland")
        {
            ParentWindow = parentHwnd,
            WindowStyle = 0x40000000 | 0x10000000, // WS_CHILD | WS_VISIBLE
            Width = width,
            Height = height
        };

        s_source = new HwndSource(parameters)
        {
            RootVisual = new TextBlock { Text = "WPF content in a Win32 window" }
        };

        return s_source.Handle;
    }
}

The same class lets a normal WPF window listen to raw messages. HwndSource.FromHwnd(handle)?.AddHook(WndProc) registers a hook that sees every message sent to the window procedure, which is the usual way to handle WM_DEVICECHANGE, WM_HOTKEY or a custom WM_APP message. Obtain the handle from WindowInteropHelper after the window’s SourceInitialized event.

WindowsFormsHost and ElementHost

The System.Windows.Forms.Integration namespace holds the two classes for mixing WPF and Windows Forms inside one process: WindowsFormsHost (a WPF element that hosts a Windows Forms control through its Child property) and ElementHost (a Windows Forms control that hosts a WPF element). They are the supported path when a Windows Forms control has no WPF equivalent, or when you migrate a Windows Forms application to WPF one form at a time.

Supported scenarios according to the documentation: hosting one or more Windows Forms controls from XAML or code, container and composite controls, ActiveX controls, master/detail layouts with either technology as the master, and hybrid controls. Going the other way, you can host WPF controls and pages in a form, start a WPF window, and host custom WPF controls. Ambient properties (font, colours, enabled state) are translated by the host control; the mapping is documented in Windows Forms and WPF Property Mapping. All interop scenarios require full trust, and both frameworks keep their single-threaded model, so cross-thread calls throw.

Behaviour Detail

Layout

A hosted Windows Forms control sometimes has a fixed size (a ComboBox has one height set by its font), does not rotate or skew, usually does not scale proportionally, and is always drawn on top of WPF elements because it is a separate HWND

Tabbing

Tab order inside the host follows the Windows Forms TabIndex; each WindowsFormsHost has its own TabIndex among its WPF siblings

Shortcut keys

Windows Forms shortcut keys handled at the preprocessing stage always take priority over WPF key bindings. A ToolStrip shortcut Ctrl+S wins over a WPF command bound to Ctrl+S regardless of focus

Accelerators

An accelerator duplicated across technologies always goes to the Windows Forms control

Enter/Leave events

Not raised when focus moves into or out of a WindowsFormsHost

Clipboard, drag-and-drop, accessibility

Work as usual across both technologies

The next example hosts a Windows Forms MaskedTextBox for an ISBN field in the Bookshelf Desktop edit form.

<Window x:Class="Bookshelf.Desktop.IsbnWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
        Title="Edit book" Height="160" Width="360">
    <StackPanel Margin="12">
        <Label Content="ISBN-13" Target="{Binding ElementName=isbnHost}"/>
        <WindowsFormsHost x:Name="isbnHost" Height="26">
            <wf:MaskedTextBox x:Name="isbnBox" Mask="000-0-0000-0000-0"/>
        </WindowsFormsHost>
    </StackPanel>
</Window>

A project that uses WindowsFormsHost must enable both stacks in its project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <UseWindowsForms>true</UseWindowsForms>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

The opposite direction, a WPF control inside a form, is code only:

using System.Windows.Forms;
using System.Windows.Forms.Integration;
using Bookshelf.Desktop.Views;

namespace Bookshelf.Desktop.Legacy;

public sealed class BookDetailsForm : Form
{
    public BookDetailsForm()
    {
        Text = "Book details";
        var host = new ElementHost
        {
            Dock = DockStyle.Fill,
            Child = new BookDetailsView() // a WPF UserControl bound to BookViewModel
        };
        Controls.Add(host);
    }
}

Source: WPF and Windows Forms interop. The examples apply the documented WindowsFormsHost and ElementHost scenarios; they are not official samples. BookDetailsView is a Bookshelf Desktop WPF view assumed for the scenario.

Pitfalls with the Forms Hosts

  • A Windows Forms shortcut (ToolStrip, MenuStrip) silently overrides a WPF KeyBinding with the same gesture. Avoid duplicate gestures across the two technologies.

  • Do not place a WindowsFormsHost inside a ScrollViewer and expect it to scroll out of view cleanly; the host ignores the parent clip.

  • Windows Forms controls that handle KeyDown themselves are processed last in a WPF host. Override IsInputKey or handle PreviewKeyDown and set IsInputKey to true when a control must see special keys.

  • There is no partial-trust story: all interop requires full trust.

Web Content: WebView2 Instead of WebBrowser

For embedding web pages, the WPF WebBrowser control is the legacy answer: it wraps an old browser engine and is not a good basis for new work. The supported replacement is WebView2, which embeds the Microsoft Edge (Chromium) engine through the WebView2 Runtime. Microsoft Learn provides getting-started tutorials for WPF and for Windows Forms. This is also the control to use when a Bookshelf Desktop screen needs rich content such as book descriptions with formatted text, or an embedded online store page.

Both tutorials install the Microsoft.Web.WebView2 NuGet package. The WPF control lives in Microsoft.Web.WebView2.Wpf and the Windows Forms control in Microsoft.Web.WebView2.WinForms; both expose a WebView2 class with a Source property for the initial URI and a CoreWebView2 property for the underlying API. The CoreWebView2 object is created asynchronously, so call await EnsureCoreWebView2Async(null) (the argument is an optional CoreWebView2Environment) before touching it.

<Window x:Class="Bookshelf.Desktop.BookPreviewWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        Title="Book preview" Height="450" Width="800">
    <DockPanel>
        <DockPanel DockPanel.Dock="Top" Margin="4">
            <Button x:Name="ButtonGo" DockPanel.Dock="Right" Click="ButtonGo_Click" Content="Go"/>
            <TextBox x:Name="addressBar"/>
        </DockPanel>
        <wv2:WebView2 x:Name="webView" Source="https://www.microsoft.com"/>
    </DockPanel>
</Window>
using System.Windows;
using Microsoft.Web.WebView2.Core;

namespace Bookshelf.Desktop;

public partial class BookPreviewWindow : Window
{
    public BookPreviewWindow()
    {
        InitializeComponent();
        webView.NavigationStarting += EnsureHttps;
        Loaded += async (_, _) => await webView.EnsureCoreWebView2Async(null);
    }

    private void ButtonGo_Click(object sender, RoutedEventArgs e)
    {
        if (webView.CoreWebView2 is not null)
        {
            webView.CoreWebView2.Navigate(addressBar.Text);
        }
    }

    private void EnsureHttps(object? sender, CoreWebView2NavigationStartingEventArgs args)
    {
        if (!args.Uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
        {
            args.Cancel = true;
        }
    }
}

Source: Get started with WebView2 in WPF apps. The sample adapts the tutorial’s address bar and NavigationStarting handler; the tutorial warns that Navigate throws an ArgumentException when the string does not begin with http:// or https://.

The Windows Forms equivalent drops a WebView2 from the toolbox and sets its Source; the control is the same WebView2 class from Microsoft.Web.WebView2.WinForms, with the same events and CoreWebView2 API. The tutorials show these navigation events, in order on the success path: NavigationStarting, SourceChanged, ContentLoading, HistoryChanged, NavigationCompleted; an HTTP redirect raises several NavigationStarting events in a row.

Host and page talk through messages. The page posts with window.chrome.webview.postMessage, and the host receives it in a CoreWebView2.WebMessageReceived handler (args.TryGetWebMessageAsString()). The host posts with CoreWebView2.PostWebMessageAsString or PostWebMessageAsJSON, which the page receives through window.chrome.webview.addEventListener with the event name message. ExecuteScriptAsync and AddScriptToExecuteOnDocumentCreatedAsync inject JavaScript. Injected scripts run after the global object is created and before any script in the document.

WebView2 and Airspace

The WebView2 WPF control is a hosted window, so it follows the airspace table above: it is drawn above other WPF elements and cannot be faded or clipped by a parent. The same Microsoft.Web.WebView2 package also offers WebView2CompositionControl, a visual-hosting version of the control that WPF content can overlap; it captures the browser output with a GraphicsCaptureSession, so frame rates can be lower and DRM-protected content does not play. The Blazor WPF tutorial for .NET 10 mentions that Blazor Hybrid switched to WebView2CompositionControl with .NET 10, which is why that tutorial asks for a Windows 10 or later target framework moniker. Check the current WebView2 API reference if an overlay on web content is a requirement.

Deployment Note

The Windows Forms tutorial states that when you distribute an app that uses WebView2 you must also distribute the WebView2 Runtime, which is then installed on the user machine. Bookshelf Desktop’s installer (see the publishing pages of this section) must therefore either bootstrap the runtime or document it as a prerequisite.

Blazor Hybrid in WPF and Windows Forms

When the UI team already has Razor components, the BlazorWebView control renders them inside a desktop window, backed by WebView2, so the same component can run in the browser, in .NET MAUI and in a WPF or Windows Forms shell. The component model, hosting and deployment of Blazor Hybrid are covered in Blazor WebAssembly, Hybrid, and Deployment; this section only shows what the Windows desktop host needs.

The official WPF tutorial for Blazor Hybrid does the following:

  1. Create a WPF project and install the Microsoft.AspNetCore.Components.WebView.Wpf package.

  2. Change the project SDK to Microsoft.NET.Sdk.Razor, and for .NET 10 give the project a Windows 10 or later target framework moniker such as net10.0-windows10.0.17763.0.

  3. Add an _Imports.razor, a wwwroot/index.html host page and the component.

  4. Declare the control in XAML and register the services in code.

<Window x:Class="Bookshelf.Desktop.BlazorWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
        xmlns:local="clr-namespace:Bookshelf.Desktop"
        Title="Blazor Hybrid" Height="450" Width="800">
    <Grid>
        <blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">
            <blazor:BlazorWebView.RootComponents>
                <blazor:RootComponent Selector="#app" ComponentType="{x:Type local:Counter}" />
            </blazor:BlazorWebView.RootComponents>
        </blazor:BlazorWebView>
    </Grid>
</Window>
using System.Windows;
using Microsoft.Extensions.DependencyInjection;

namespace Bookshelf.Desktop;

public partial class BlazorWindow : Window
{
    public BlazorWindow()
    {
        InitializeComponent();

        var serviceCollection = new ServiceCollection();
        serviceCollection.AddWpfBlazorWebView();
        Resources.Add("services", serviceCollection.BuildServiceProvider());
    }
}

Source: Build a Windows Presentation Foundation (WPF) Blazor app. The page’s component is the tutorial’s Counter; replace it with a Bookshelf Desktop component such as a book list that calls IBookRepository.

Windows Forms has a parallel tutorial and package for its own BlazorWebView; it is not reproduced here. Because BlazorWebView is built on WebView2, it is a hosted HWND-style surface too, so the airspace rules apply to the control as a whole.

Calling the Operating System: P/Invoke and CsWin32

P/Invoke is the .NET mechanism for calling an unmanaged function exported by a DLL. The details (DllImport, LibraryImport, marshalling, SafeHandle, COM interop) are covered in the C# reference page Native Interop; do not repeat them here. This section only adds the Windows-desktop angle.

Hand-writing P/Invoke signatures is the main source of interop bugs: wrong integer widths, wrong string character set, missing SetLastError, leaked handles. CsWin32 removes the guesswork. It is a Microsoft-owned source generator that creates strongly typed P/Invoke declarations from the Windows API metadata. It is a library hosted on GitHub, not a Microsoft Learn topic.

The workflow:

  1. Add the package: dotnet add package Microsoft.Windows.CsWin32.

  2. Create a NativeMethods.txt file next to the project file and list one API name per line. A line can be a function, a struct, an enum, a constant, an interface, a module wildcard such as Kernel32.*, or a constant prefix ending in a star. Lines starting with // are comments, and a leading - excludes a name.

  3. Call the generated methods through the Windows.Win32.PInvoke class, which is the default name.

  4. Tune generation (class name, visibility, allowMarshaling) in an optional NativeMethods.json.

// NativeMethods.txt for Bookshelf Desktop
FlashWindowEx
GetForegroundWindow
MessageBeep
SetForegroundWindow
FLASHWINFO
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;

namespace Bookshelf.Desktop.Services;

public static class TaskbarAttention
{
    /// <summary>Flashes the taskbar button until the window comes to the foreground.</summary>
    public static void Flash(nint hwnd)
    {
        FLASHWINFO info = new()
        {
            cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf<FLASHWINFO>(),
            hwnd = new HWND(hwnd),
            dwFlags = FLASHWINFO_FLAGS.FLASHW_ALL | FLASHWINFO_FLAGS.FLASHW_TIMERNOFG,
            uCount = 0,
            dwTimeout = 0
        };

        PInvoke.FlashWindowEx(in info);
    }
}

Source: CsWin32 on GitHub. The repository page and its getting-started guide document the package name, NativeMethods.txt and the PInvoke class. The FlashWindowEx call above is a Bookshelf Desktop illustration; the generated member names and overloads depend on the Win32 metadata version, so IntelliSense on the generated PInvoke class shows the exact signatures.

The package turns on AllowUnsafeBlocks for you because generated code often uses pointers; leaving it enabled is the simplest choice. It needs the .NET 8 SDK or a recent Visual Studio, and works for .NET Framework and .NET Standard targets with a few extra packages. Neither WPF nor Windows Forms supports Native AOT or trimming, so the reflection-free nature of LibraryImport is a code-quality benefit here rather than a requirement.

Windows Runtime and Windows App SDK APIs in a WPF or WinForms App

A WPF or Windows Forms app on .NET can call Windows Runtime (WinRT) APIs and adopt parts of the Windows App SDK without becoming a WinUI app. Microsoft’s modernization guide lists three independent approaches that you can combine: calling WinRT APIs, adding the Windows App SDK, and adopting MSIX packaging.

For WinRT APIs on .NET 6 and later, the mechanism is the target framework moniker. A Windows-version-specific TFM adds a reference to the Windows SDK targeting package at build time:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
    <SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
</Project>

The documented TFM values for .NET 10 include net10.0-windows10.0.26100.0 (Windows 11 24H2), net10.0-windows10.0.22621.0 (22H2), net10.0-windows10.0.22000.0 (initial Windows 11) and net10.0-windows10.0.19041.0 (Windows 10 2004). The version in the TFM selects which APIs you compile against; it does not set the minimum operating system. Set SupportedOSPlatformVersion for that, and guard calls to newer APIs with ApiInformation checks. With a lower supported version, Visual Studio raises warning CA1416 for calls that need a runtime check.

Several WinRT types in Windows.UI are not supported on .NET 6 and later and have Windows App SDK replacements: Windows.UI.Colors and ColorHelper move to Microsoft.UI, most of Windows.UI.Text to Microsoft.UI.Text, and all of Windows.UI.Xaml to Microsoft.UI.Xaml.

Feature Windows App SDK needed Package identity needed

Windows AI Foundry

No

Yes

Host WinUI controls with XAML Islands

Yes

No

DWriteCore text rendering

Yes

No

MRT Core resource management

Yes

No

Background tasks (Windows App SDK)

Yes

Yes

Startup tasks and file associations

No

Yes

Windows Widgets

No

Yes

Mica or Acrylic on Win32 windows, rounded corners

No

No

Credential locker, cryptography, share sheet integration

No

No

The starting point for a WPF or Windows Forms project is the Windows App SDK page Use the Windows App SDK in an existing project. Package identity (what MSIX packaging or a sparse package grants) is covered in the platform integration and publishing pages of this section.

Pitfalls with WinRT Calls

  • Pickers and some dialogs need the owner window handle. A desktop app must associate the WinRT object with its window through the IInitializeWithWindow pattern; the cited modernization page does not describe this, so consult the picker documentation for the API you call.

  • A TFM that is newer than the machine’s Windows version still compiles; the failure happens at run time. Use SupportedOSPlatformVersion and runtime checks.

  • Using Windows.UI.Xaml types from .NET 6 or later does not work; use the Microsoft.UI.Xaml equivalents.

WinUI XAML Islands

Developers often ask for WinUI 3 controls inside an existing WPF or Windows Forms window. The mechanism is WinUI XAML Islands, built on the DesktopWindowXamlSource class in Microsoft.UI.Xaml.Hosting (Windows App SDK). Microsoft Learn describes it as the primary XAML hosting API: it connects Microsoft.UI.Xaml content to a native child window, and works for WPF, Windows Forms and Win32 hosts. The key members are Content (the hosted UIElement), Initialize (takes a WindowId), SiteBridge (for moving and resizing the child site), NavigateFocus, TakeFocusRequested, HasFocus and Close/Dispose.

The honest status, from the Windows App SDK documentation and not from a marketing page:

  • There are no ready-made WPF or Windows Forms wrapper controls for WinUI XAML Islands in the Windows App SDK. The Learn page states that DesktopWindowXamlSourceControl in the official sample is an application-defined helper, not an SDK control. You write the host control yourself.

  • The host owns a lot of plumbing: runtime initialization (the Windows App SDK runtime, through the bootstrapper or <WindowsPackageType>None</WindowsPackageType> for unpackaged apps), a single-threaded apartment UI thread, a dispatcher queue, a Microsoft.UI.Xaml.Application object, XAML metadata providers for each WinUI class library, WindowsXamlManager.InitializeForCurrentThread, and message pre-translation through the native ContentPreTranslateMessage function for input.

  • Layout, DPI, focus and cleanup order are the host’s responsibility.

  • It is distinct from the older system XAML Islands, which host UWP controls from Windows.UI.Xaml. The Learn page tells you not to combine the APIs or setup steps of the two implementations. The old community WindowsXamlHost wrapper controls belong to that legacy path.

The following excerpt is the core of a custom Windows Forms host control, taken from the Learn page. It assumes the runtime, dispatcher queue and XAML environment are already initialized and that IslandView is a WinUI UserControl from a referenced WinUI class library:

using System.ComponentModel;
using Microsoft.UI;
using Microsoft.UI.Xaml.Hosting;
using Windows.Graphics;

internal sealed class WinUIIslandHost : Control
{
    private DesktopWindowXamlSource? _xamlSource;

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);

        if (DesignMode || LicenseManager.UsageMode == LicenseUsageMode.Designtime)
        {
            return;
        }

        _xamlSource = new DesktopWindowXamlSource();
        _xamlSource.Initialize(Win32Interop.GetWindowIdFromWindow(Handle));
        _xamlSource.Content = new MyWinUILibrary.IslandView();
        ResizeIsland();
    }

    protected override void OnSizeChanged(EventArgs e)
    {
        base.OnSizeChanged(e);
        ResizeIsland();
    }

    private void ResizeIsland() =>
        _xamlSource?.SiteBridge.MoveAndResize(new RectInt32(0, 0, ClientSize.Width, ClientSize.Height));

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _xamlSource?.Dispose();
            _xamlSource = null;
        }

        base.Dispose(disposing);
    }
}

Source: Host WinUI controls with XAML Islands. The excerpt is condensed from the page’s sample (the page’s version also releases the island in OnHandleDestroyed). The maintained sample is the Islands sample in the WindowsAppSDK-Samples repository, which uses an unpackaged .NET 8 Windows Forms host.

A decision rule that follows from the documentation: use an island when one valuable surface needs WinUI controls and the rest of the application should stay as it is. When most screens would change, or maintaining two UI frameworks costs more than it saves, migrate the whole UI to WinUI 3; Microsoft’s migration decision guide covers that choice. Reference documentation for the class is at DesktopWindowXamlSource class.

Pitfalls

  • Putting WPF content over a hosted window. An Opacity animation, a drop shadow, a busy overlay or an in-window dialog placed over a WebView2, WindowsFormsHost or HwndHost will be hidden by it. Plan the layout around the hosted rectangle or use separate windows.

  • Forgetting to keep the HwndSource alive. An HwndSource held only by a local variable can be collected and the window disappears. Store it in a field.

  • Calling CoreWebView2 before it exists. CoreWebView2 is null until EnsureCoreWebView2Async completes. Guard every use, as the tutorial does with its CoreWebView2 != null check.

  • Skipping the runtime. WebView2 and BlazorWebView need the WebView2 Runtime on the target machine; WinUI XAML Islands need the Windows App SDK runtime. Test on a clean machine.

  • Leaking handles. BuildWindowCore creates an HWND; DestroyWindowCore must destroy it. Wrap other native handles in SafeHandle types, as the C# native interop page describes.

  • Hand-written P/Invoke. Prefer CsWin32 or LibraryImport over copied signatures from old forums; wrong widths on 64-bit cause crashes that only show up on some machines.

  • Cross-thread calls. Both WPF and Windows Forms assume a single-threaded model; touching a hosted control from another thread throws while debugging.

  • Multilevel hybrid nesting. Windows Forms in WPF in Windows Forms is documented as unsupported.

  • Mixing legacy and current XAML Islands. Windows.UI.Xaml and Microsoft.UI.Xaml hosting APIs are different implementations; follow only one set of instructions.

  • Native AOT and trimming. They are not supported for WPF and Windows Forms, whatever interop you add. The platforms page of this section explains the status.