WPF to WinUI 3 Migration Skill
Migrate PowerToys modules from WPF (System.Windows.*) to WinUI 3 (Microsoft.UI.Xaml.* / Windows App SDK). Based on patterns validated in the ImageResizer module migration.
When to Use This Skill
- Migrate a PowerToys module from WPF to WinUI 3
- Convert WPF XAML files to WinUI 3 XAML
- Replace
System.Windowsnamespaces withMicrosoft.UI.Xaml - Migrate
Dispatcherusage toDispatcherQueue - Migrate custom
Observable/RelayCommandto CommunityToolkit.Mvvm source generators - Replace WPF-UI (Lepo) controls with native WinUI 3 controls
- Convert imaging code from
System.Windows.Media.ImagingtoWindows.Graphics.Imaging - Handle WPF
Windowvs WinUIWindowdifferences (sizing, positioning, SizeToContent) - Migrate resource files from
.resxto.reswwithResourceLoader - Fix installer/build pipeline issues after WinUI 3 migration
- Update project files, NuGet packages, and signing config
Prerequisites
- Visual Studio 2022 17.4+
- Windows App SDK NuGet package (
Microsoft.WindowsAppSDK) - .NET 8+ with
net8.0-windows10.0.19041.0TFM - Windows 10 1803+ (April 2018 Update or newer)
Migration Strategy
Phase-by-Phase Scope
Work on bounded problems, not the entire codebase at once. Each phase should compile before moving to the next.
- Project file — Update TFM, NuGet packages, set
<UseWinUI>true</UseWinUI> - Data models and business logic — No UI dependencies, migrate first
- MVVM framework — Replace custom Observable/RelayCommand with CommunityToolkit.Mvvm
- Resource strings — Migrate
.resx→.resw, introduceResourceLoaderInstance - Services and utilities — Replace
System.Windowstypes, async-ify imaging code - ViewModels — Update Dispatcher usage, binding patterns
- Views/Pages — Starting from leaf pages with fewest dependencies
- Main page / shell — Last, since it depends on everything
- App.xaml / startup code — Merge carefully (do NOT overwrite WinUI 3 boilerplate)
- Installer & build pipeline — Update WiX, signing, build events
- Tests — Adapt for WinUI 3 runtime, async patterns
Migration Contract: Prohibited Patterns
These rules capture human judgment and must be applied consistently across every file. Do NOT deviate.
Architecture prohibitions:
- Do NOT overwrite
App.xaml/App.xaml.cs— WinUI 3 has different lifecycle boilerplate. Merge resources and init code into the generated WinUI 3 App class. - Do NOT create Exe→WinExe
ProjectReference— Extract shared code to a Library project. Causes phantom build artifacts. - Do NOT instantiate services directly — Use DI and CommunityToolkit.Mvvm patterns.
- Do NOT create a
Windowsubclass for every dialog or sub-page — useContentDialogfor in-app dialogs andFrame/Pagenavigation for sub-views. SeparateWindowclasses are reserved for distinct top-level surfaces (e.g., FancyZones editor, OOBE). - Do NOT omit
WindowsPackageType=NoneandWindowsAppSDKSelfContained=true— Both are mandatory in the csproj for every WinUI 3 module in PowerToys. Without them the app crashes at startup withCOMException: ClassFactory cannot supply requested classbecause the WinUI 3 runtime DLLs are not found. - Do NOT default to a bare
Window, and do NOT hand-write code-behind for windowing that XAML can express — For a top-level window in a WinUI 3 module, useWinUIEx.WindowExor an existing PowerToys base derived from it (for example,TransparentWindowfor transient overlays).WindowExrestores WPF-likeWindowmembers as XAML properties (MinWidth/MinHeight,Width/Height,IsResizable,IsMaximizable/IsMinimizable,IsTitleBarVisible,IsAlwaysOnTop,IsShownInSwitchers,WindowState,SystemBackdrop) plus helpers (CenterOnScreen(),PersistenceId), so windowing is declared in XAML instead of manualAppWindow/OverlappedPresentercode-behind. This is the established PowerToys convention across ImageResizer, PowerDisplay, Peek, AdvancedPaste, MeasureTool, ShortcutGuide, Settings, Hosts, FileLocksmith, QuickAccess, and other WinUI 3 modules. Only drop to rawAppWindow/presenter code for behaviorWindowExdoes not expose. See Threading and Window Management → WindowEx.
XAML prohibitions:
- Do NOT use
{DynamicResource}— Replace with{ThemeResource}(theme-reactive) or{StaticResource}. - Do NOT use
{Binding}inSetter.Value— Not supported in WinUI 3. Use{StaticResource}. - Do NOT use
{x:Static}— Replace with{x:Bind},x:Uid, or code-behind. - Do NOT use
{x:Type}— Not supported. Usex:DataTypefor DataTemplate, or code-behind. - Do NOT use
clr-namespace:— Replace withusing:in all xmlns declarations. - Do NOT use
Style.Triggers/DataTrigger/EventTrigger— Replace withVisualStateManager. - Do NOT use
MultiBinding— Replace withx:Bindfunction binding or computed ViewModel property. - Do NOT mechanically port WPF
IValueConverterclasses — Prefer controlVisualStates, direct{x:Bind}Boolean-to-Visibilityconversion, resources supplied byXamlControlsResources, orCommunityToolkit.WinUI.Converters. Reuse converter instances and invert them withConverterParameter=Truewhen supported; write a custom converter only for app-specific conversion logic. See Value Converter Decision Guide. - Do NOT use
Visibility="Hidden"— WinUI only hasVisibleandCollapsed. UseOpacity="0"if layout must be preserved. - Do NOT use
IsDefault/IsCancel— UseAccentButtonStylefor primary button; handle Enter/Escape in code-behind. - Do NOT omit
BasedOnwhen overriding default styles — Without it, your style replaces the entire default. Always useBasedOn="{StaticResource DefaultButtonStyle}"etc. - Do NOT omit
XamlControlsResourcesas first merged dictionary — It provides default Fluent styles. Without it, controls have no visual appearance.
Code-behind prohibitions:
- Do NOT use
Application.Current.Dispatcher— StoreDispatcherQueuein a static field explicitly. - Do NOT use
Window.Current— Not supported. Use a customApp.Windowstatic property. - Do NOT put
DataContext,Resources, orVisualStateManageronWindow— WinUI 3Windowis NOT aDependencyObject. Use a rootPage/UserControl/Grid. - Do NOT use tunneling/preview events (
PreviewMouseDown,PreviewKeyDown) — WinUI has no tunneling. Use bubbling equivalents withHandledproperty orAddHandler(handledEventsToo: true).
Resource prohibitions:
- Do NOT use
Properties.Resources.MyString— Replace withResourceLoaderInstance.ResourceLoader.GetString("MyString"). - Do NOT initialize
ResourceLoader-dependent values as static fields — Wrap inLazy<T>or null-coalescing property. - Do NOT use
pack://URIs — Replace withms-appx:///scheme.
Quick Reference Tables
Namespace Mapping
| WPF | WinUI 3 | Notes |
|---|---|---|
System.Windows |
Microsoft.UI.Xaml |
Root namespace |
System.Windows.Controls |
Microsoft.UI.Xaml.Controls |
Core controls |
System.Windows.Controls.Primitives |
Microsoft.UI.Xaml.Controls.Primitives |
Low-level primitives |
System.Windows.Media |
Microsoft.UI.Xaml.Media |
Brushes, transforms |
System.Windows.Media.Animation |
Microsoft.UI.Xaml.Media.Animation |
Storyboard, animations |
System.Windows.Media.Imaging |
Microsoft.UI.Xaml.Media.Imaging (UI) / Windows.Graphics.Imaging (processing) |
Split by purpose |
System.Windows.Media.Media3D |
No equivalent | Use Win2D or Composition APIs |
System.Windows.Shapes |
Microsoft.UI.Xaml.Shapes |
Rectangle, Ellipse, Path |
System.Windows.Input |
Microsoft.UI.Xaml.Input |
Pointer, keyboard, focus |
System.Windows.Data |
Microsoft.UI.Xaml.Data |
Binding, IValueConverter |
System.Windows.Documents |
Microsoft.UI.Xaml.Documents |
Limited — RichTextBlock + Paragraph |
System.Windows.Markup |
Microsoft.UI.Xaml.Markup |
XAML parsing, markup extensions |
System.Windows.Automation |
Microsoft.UI.Xaml.Automation |
Accessibility / UI Automation |
System.Windows.Navigation |
No direct equivalent | Use Frame.Navigate() |
System.Windows.Threading |
Microsoft.UI.Dispatching |
Dispatcher → DispatcherQueue |
System.Windows.Interop |
WinRT.Interop / Microsoft.UI.Xaml.Hosting |
HWND interop |
Control Replacements (No 1:1 Mapping)
These WPF controls have no direct counterpart and require a different control or third-party package:
| WPF Control | WinUI 3 Replacement | Notes |
|---|---|---|
DataGrid |
WinUI.TableView |
Community library; the Toolkit DataGrid is no longer maintained. Legacy code may still pin v7 CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2 |
Ribbon |
CommandBar / NavigationView, or Toolkit Labs Ribbon |
No first-party Ribbon in WinUI; Labs component is experimental/partial |
Menu / MenuItem |
MenuBar / MenuBarItem / MenuFlyout |
MenuBar for classic menu, MenuFlyout for context |
ContextMenu |
MenuFlyout |
Assign to ContextFlyout property |
ToolBar / ToolBarTray |
CommandBar + AppBarButton |
|
StatusBar |
Custom Grid/StackPanel or InfoBar |
No StatusBar control |
TabControl |
TabView or NavigationView (top mode) |
TabView for closeable tabs |
DocumentViewer |
WebView2 |
Render PDFs/XPS inside WebView2 |
FlowDocument |
RichTextBlock |
Partial replacement only |
RichTextBox |
RichEditBox |
Rich text editing |
GroupBox |
Expander (built-in) or HeaderedContentControl (Toolkit) |
See Layout & Header Controls from CommunityToolkit.WinUI below |
Label |
TextBlock |
WPF Label is a ContentControl; use TextBlock + AccessKey |
TreeView |
TreeView (native) |
Available natively, but data binding model differs significantly |
MessageBox |
ContentDialog |
Must set XamlRoot before ShowAsync() |
MediaElement |
MediaPlayerElement |
Different API |
AccessText |
Not available | Use AccessKey property on target control |
Layout & Header Controls from CommunityToolkit.WinUI
These WPF controls have no built-in WinUI 3 equivalent — install the corresponding CommunityToolkit package. The NuGet package id and the XAML namespace differ intentionally: package names end in .Primitives / .HeaderedControls, but the registered XAML namespace is the shorter CommunityToolkit.WinUI.Controls (confirmed in the official Microsoft Q&A).
| WPF Control | WinUI 3 Replacement | NuGet Package | XAML Namespace |
|---|---|---|---|
WrapPanel |
WrapPanel |
CommunityToolkit.WinUI.Controls.Primitives |
using:CommunityToolkit.WinUI.Controls |
UniformGrid |
UniformGrid |
CommunityToolkit.WinUI.Controls.Primitives |
using:CommunityToolkit.WinUI.Controls |
DockPanel |
DockPanel |
CommunityToolkit.WinUI.Controls.Primitives |
using:CommunityToolkit.WinUI.Controls |
GroupBox (alt.) |
HeaderedContentControl |
CommunityToolkit.WinUI.Controls.HeaderedControls |
using:CommunityToolkit.WinUI.Controls |
No Equivalent — Requires Architectural Rework
These WPF features have no WinUI counterpart and require redesign, not find-and-replace:
| WPF Feature | WinUI 3 Replacement Strategy |
|---|---|
Style.Triggers / DataTrigger |
VisualStateManager with StateTrigger — see XAML Migration |
MultiBinding |
x:Bind function binding: {x:Bind local:Converters.Format(VM.A, VM.B), Mode=OneWay} |
RoutedUICommand / CommandBinding |
ICommand / [RelayCommand] from CommunityToolkit.Mvvm. WinUI also has StandardUICommand / XamlUICommand for platform commands. |
AdornerLayer / Adorner |
Depends on use case: TeachingTip/InfoBar (validation), Popup (overlays), PlaceholderText (watermarks), Canvas overlay (decorations) |
Visibility.Hidden |
Opacity="0" with Visibility="Visible" (preserves layout space) |
Window.Resources / Window.DataContext |
Move to root Grid.Resources / root Page/UserControl — WinUI Window is NOT a DependencyObject |
Tunneling events (Preview*) |
Use bubbling equivalents + Handled property or AddHandler(handledEventsToo: true) |
Critical API Replacements
| WPF | WinUI 3 | Notes |
|---|---|---|
Dispatcher.Invoke() |
DispatcherQueue.TryEnqueue() |
Different return type (bool), async by default |
Dispatcher.CheckAccess() |
DispatcherQueue.HasThreadAccess |
Property vs method |
Application.Current.Dispatcher |
Store DispatcherQueue in static field |
See Threading |
Window.Current |
Custom App.Window static property |
Not supported in Windows App SDK |
Application.Current.MainWindow |
Custom App.Window static property |
Must track manually |
MessageBox.Show() |
ContentDialog |
Must set XamlRoot |
System.Windows.Clipboard |
Windows.ApplicationModel.DataTransfer.Clipboard |
Different API surface |
RoutedUICommand / CommandBinding |
ICommand / [RelayCommand] |
Remove CommandBinding; bind ICommand directly |
Properties.Resources.MyString |
ResourceLoaderInstance.ResourceLoader.GetString("MyString") |
Lazy-init pattern |
DynamicResource |
ThemeResource |
Theme-reactive only |
clr-namespace: |
using: |
XAML namespace prefix |
{x:Static props:Resources.Key} |
x:Uid or ResourceLoader.GetString() |
.resx → .resw |
DataType="{x:Type m:Foo}" |
x:DataType="m:Foo" |
x:Type not supported |
SizeToContent="Height" |
Custom SizeToContent() via AppWindow.Resize() |
See Windowing |
Pack URI (pack://...) |
ms-appx:/// |
Resource URI scheme |
Observable (custom base) |
ObservableObject + [ObservableProperty] |
CommunityToolkit.Mvvm |
RelayCommand (custom) |
[RelayCommand] source generator |
CommunityToolkit.Mvvm |
JpegBitmapEncoder |
BitmapEncoder.CreateAsync(JpegEncoderId, stream) |
Async, unified API |
encoder.QualityLevel = 85 |
BitmapPropertySet { "ImageQuality", 0.85f } |
int 1-100 → float 0-1 |
Event Replacements (Mouse → Pointer)
| WPF Event | WinUI 3 Event | Notes |
|---|---|---|
MouseLeftButtonDown |
PointerPressed |
Check IsLeftButtonPressed on args |
MouseLeftButtonUp |
PointerReleased |
Check pointer properties |
MouseRightButtonDown |
RightTapped |
Or PointerPressed with right button check |
MouseMove |
PointerMoved |
MouseEventArgs → PointerRoutedEventArgs |
MouseWheel |
PointerWheelChanged |
Different event args |
MouseEnter / MouseLeave |
PointerEntered / PointerExited |
|
MouseDoubleClick |
DoubleTapped |
Different event args |
PreviewMouseDown |
PointerPressed |
No tunneling — use Handled or AddHandler |
PreviewKeyDown |
KeyDown |
KeyEventArgs → KeyRoutedEventArgs |
Property Replacements
| WPF | WinUI 3 | Context |
|---|---|---|
Visibility.Hidden |
Visibility.Collapsed or Opacity="0" |
Use Opacity="0" to preserve layout |
TextWrapping.WrapWithOverflow |
TextWrapping.Wrap |
WinUI doesn't distinguish |
Focusable="True" |
IsTabStop="True" |
Different property name |
ContextMenu= |
ContextFlyout= |
On any UIElement |
MediaElement |
MediaPlayerElement |
Different API |
SnapsToDevicePixels |
Not available | WinUI handles pixel snapping internally |
NuGet Package Migration
| WPF | WinUI 3 | Notes |
|---|---|---|
Microsoft.Xaml.Behaviors.Wpf |
Microsoft.Xaml.Behaviors.WinUI.Managed |
|
WPF-UI (Lepo) |
Remove — use native WinUI 3 controls | |
CommunityToolkit.Mvvm |
CommunityToolkit.Mvvm (same) |
|
Microsoft.Toolkit.Wpf.* |
CommunityToolkit.WinUI.* |
|
| (none) | Microsoft.WindowsAppSDK |
Required |
| (none) | Microsoft.Windows.SDK.BuildTools |
Required |
| (none) | WinUIEx |
Recommended for top-level windows in WinUI 3 modules — add <PackageReference Include="WinUIEx" /> (the version is centrally managed), then use WindowEx or an existing PowerToys base derived from it. It exposes WPF-like window properties in XAML (size, min/max, resizable, title-bar visibility, backdrop, always-on-top) plus CenterOnScreen()/PersistenceId; prefer it over bare Window and manual AppWindow code-behind |
| (none) | CommunityToolkit.WinUI.Converters |
Optional |
| (none) | CommunityToolkit.WinUI.Controls.Primitives |
Optional — WrapPanel, UniformGrid, DockPanel, ConstrainedBox |
| (none) | CommunityToolkit.WinUI.Controls.HeaderedControls |
Optional — HeaderedContentControl, HeaderedItemsControl, HeaderedTreeView |
| (none) | CommunityToolkit.WinUI.Controls.SettingsControls |
Optional — SettingsCard, SettingsExpander |
| (none) | CommunityToolkit.WinUI.Controls.Sizers |
Optional — GridSplitter |
| (none) | CommunityToolkit.WinUI.UI.Controls.DataGrid |
Legacy v7 — only for migrating existing DataGrid code; prefer WinUI.TableView |
XAML Syntax Changes
| WPF | WinUI 3 | Notes |
|---|---|---|
xmlns:local="clr-namespace:MyApp" |
xmlns:local="using:MyApp" |
CLR → using syntax |
{DynamicResource Key} |
{ThemeResource Key} |
Re-evaluates on theme change |
{StaticResource Key} |
{StaticResource Key} |
Same — resolved once at load |
{x:Static Type.Member} |
{x:Bind} or code-behind |
|
{x:Type local:MyType} |
Not supported | Use x:DataType for DataTemplate |
{x:Array} |
Not supported | Create collections in code-behind |
<Style.Triggers> / <DataTrigger> |
VisualStateManager |
See XAML Migration |
{Binding} in Setter.Value |
Not supported — use StaticResource |
|
Content="{x:Static p:Resources.Cancel}" |
x:Uid="Cancel" with .Content in .resw |
|
sys:String / sys:Int32 / etc. |
x:String / x:Int32 / etc. |
XAML intrinsic types |
<ui:FluentWindow> (WPF-UI) |
<Window> |
Native + ExtendsContentIntoTitleBar |
<ui:NumberBox> / <ui:ProgressRing> (WPF-UI) |
Native <NumberBox> / <ProgressRing> |
|
BasedOn="{StaticResource {x:Type ui:Button}}" |
BasedOn="{StaticResource DefaultButtonStyle}" |
Named style keys |
IsDefault="True" / IsCancel="True" |
Style="{StaticResource AccentButtonStyle}" / KeyDown |
|
<AccessText> |
Not available — use AccessKey property |
|
<behaviors:Interaction.Triggers> |
Code-behind or WinUI behaviors | |
Window.Resources |
Root container's Resources (e.g. Grid.Resources) |
Window is not a DependencyObject |
Binding: {Binding} vs {x:Bind}
Both work in WinUI 3. Prefer {x:Bind} for new/migrated code.
| Feature | {Binding} |
{x:Bind} |
|---|---|---|
| Default mode | OneWay |
OneTime — add Mode=OneWay explicitly! |
| Default source | DataContext |
Page/UserControl code-behind |
| Compile-time validation | No | Yes |
| Function binding | No | Yes (replaces MultiBinding) |
| Performance | Reflection-based | Compiled, no reflection |
MultiBinding support |
No (not in WinUI) | Use function binding |
Detailed Reference Docs
Read only the section relevant to your current task:
- Namespace and API Mapping — Full type mapping, NuGet changes, project file, CsWinRT interop
- XAML Migration Guide — XAML syntax, WPF-UI removal, markup extensions, styles, resources, data binding
- Threading and Window Management — Dispatcher, DispatcherQueue, SizeToContent, AppWindow, HWND interop, custom entry point
- Imaging API Migration — BitmapEncoder/Decoder, SoftwareBitmap, CodecHelper, async patterns, int→uint
- PowerToys-Specific Patterns — MVVM migration, ResourceLoader, Lazy init, installer, signing, test adaptation, build pipeline
Common Pitfalls (from ImageResizer migration)
| Pitfall | Solution |
|---|---|
ContentDialog throws "does not have a XamlRoot" |
Set dialog.XamlRoot = this.Content.XamlRoot before ShowAsync() |
FilePicker throws error in desktop app |
Call WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd) |
Window.Dispatcher returns null |
Use Window.DispatcherQueue instead |
Resources on Window element not found |
Move resources to root layout container (Grid.Resources) |
VisualStateManager on Window fails |
Use UserControl or Page inside the Window |
Satellite assembly installer errors (WIX0103) |
Remove .resources.dll refs from Resources.wxs; WinUI 3 uses .pri |
Phantom .exe/.deps.json in root output dir |
Avoid Exe→WinExe ProjectReference; use Library project |
ResourceLoader crash at static init |
Wrap in Lazy<T> or null-coalescing property — see Lazy Init |
SizeToContent not available |
Implement manual content measurement + AppWindow.Resize() with DPI scaling |
x:Bind default mode is OneTime |
Explicitly set Mode=OneWay or Mode=TwoWay |
DynamicResource / x:Static not compiling |
Replace with ThemeResource / ResourceLoader or x:Uid |
IValueConverter.Convert signature mismatch |
Last param: CultureInfo → string (language tag) |
| Test project can't resolve WPF types | Add <UseWPF>true</UseWPF> temporarily; remove after imaging migration |
Pixel dimension type mismatch (int vs uint) |
WinRT uses uint for pixel sizes — add u suffix in test assertions |
$(SolutionDir) empty in standalone project build |
Use $(MSBuildThisFileDirectory) with relative paths instead |
| JPEG quality value wrong after migration | WPF: int 1-100; WinRT: float 0.0-1.0 |
| MSIX packaging fails in PreBuildEvent | Move to PostBuildEvent; artifacts not ready at PreBuild time |
| RC file icon path with forward slashes | Use double-backslash escaping: ..\\ui\\Assets\\icon.ico |
COMException: ClassFactory cannot supply requested class at startup |
Missing <WindowsPackageType>None</WindowsPackageType> and/or <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained> in csproj. Without these, the app tries to locate the Windows App SDK framework package (not installed) instead of using bundled runtime DLLs. Both properties are mandatory for every WinUI 3 module in PowerToys. |
CombinedGeometry not available in WinUI 3 |
WinUI 3 UIElement.Clip only accepts RectangleGeometry. For overlay hole effects (exclude region), use a Path element with GeometryGroup FillRule="EvenOdd" containing two RectangleGeometry children — the EvenOdd rule creates a transparent hole where geometries overlap. |
Troubleshooting
| Issue | Solution |
|---|---|
| Build fails after namespace rename | Check for lingering System.Windows usings; some types have no direct equivalent |
Missing PresentationCore.dll at runtime |
Ensure ALL imaging code uses Windows.Graphics.Imaging, not System.Windows.Media.Imaging |
DataContext not working on Window |
WinUI 3 Window is not a DependencyObject; use a root Page or UserControl |
| XAML designer not available | WinUI 3 does not support XAML Designer; use Hot Reload instead |
| NuGet restore failures | Run build-essentials.cmd after adding Microsoft.WindowsAppSDK package |
Parallel.ForEach compilation error |
Migrate to Parallel.ForEachAsync for async imaging operations |
| Signing check fails on leaked artifacts | Run generateAllFileComponents.ps1; verify only WinUI3Apps\\ paths in signing config |
COMException / ClassFactory error at app launch |
Ensure csproj has <WindowsPackageType>None</WindowsPackageType> and <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>. These are required for all unpackaged WinUI 3 apps in PowerToys — without them the WinUI 3 COM runtime cannot be found. |