name: wpf-to-uno-migration description: "Migrate WPF desktop applications to cross-platform Uno Platform apps. Phased workflow covering namespace/API mapping, XAML replacements, NavigationView region setup, Material Design theming, settings with IWritableOptions, service abstraction with DI, and platform-specific UI patterns. Use when: (1) Planning or executing a WPF-to-Uno migration, (2) Converting WPF Windows/dialogs to Uno Pages/ContentDialogs, (3) Replacing WPF settings/singletons with Uno Extensions DI, (4) Adapting WPF controls to WinUI/Material equivalents, (5) Debugging blank pages from bad resource keys or NavigationView misconfiguration. Do NOT use for: Silverlight migration (see uno-migration-troubleshoot), UWP migration, new Uno project setup (see uno-platform-agent), or general XAML optimization (see winui-xaml)." license: "Apache 2.0 (patterns derived from Uno Platform documentation and production migrations)" metadata: version: "1.0.0" author: "Uno Platform" category: "uno-platform-migration" tags: [wpf, migration, cross-platform, uno-platform, winui]
WPF to Uno Platform Migration
Phased workflow for migrating WPF desktop applications to cross-platform Uno Platform apps. Patterns are battle-tested from production migrations (37K LOC WPF app → 13.2K LOC Uno app at ~75% verified feature parity).
Before You Start
Prerequisites
- Install tooling: Latest .NET SDK, Uno Platform templates (
dotnet new install Uno.Templates), anduno-check - Run
uno-checkto validate your environment before writing any code - Audit your WPF codebase for migration scope:
- Count usages of
x:Static,MultiBinding,Style.Triggers,DataTrigger(all unsupported — need replacements) - Identify
DataGrid, customWindowsubclasses, WPF-UI controls (all need alternatives) - List all
System.Windows.*namespace imports (all change toMicrosoft.UI.Xaml.*) - Catalog platform-specific APIs: clipboard, file system, process, registry, COM (need
#if WINDOWSguards)
- Count usages of
Key Architectural Decisions
| WPF Pattern | Uno Platform Replacement | Why |
|---|---|---|
| Multiple Windows | Single Window + Page navigation | Cross-platform apps use a single-window model |
| Static singletons | DI with IServiceProvider |
Testable, cross-platform, MVUX-compatible |
Properties.Settings.Default |
IWritableOptions<T> |
Reactive, type-safe, persisted automatically |
| Code-behind event handlers | MVUX Models with IState<T> / IFeed<T> |
Reactive data flow, no manual UI updates |
| WPF MVVM (ICommand) | MVUX auto-generated commands | Public async methods become commands automatically |
RoutedCommand / CommandBindings |
Direct method calls + KeyboardAccelerators |
WPF command routing doesn't exist in WinUI |
Process.Start(url) |
Launcher.LaunchUriAsync(uri) |
Cross-platform (desktop, Wasm) |
Migration Workflow
Phase 1: Scaffold (~500 LOC)
- Create reference project alongside your WPF solution:
dotnet new unoapp -o MyApp.Uno --preset=recommended - Verify it builds:
dotnet build MyApp.Uno - Establish shared code project (optional): portable models, interfaces, and utilities that both WPF and Uno can reference during incremental migration
- Set up GlobalUsings.cs to reduce per-file import noise:
global using MyApp; global using MyApp.Interfaces; global using MyApp.Models; global using MyApp.Services;
Phase 2: Shell and First Page (~3,000 LOC)
- Build the Shell with NavigationView — this is the most critical architecture decision. See references/03-architecture-and-navigation.md for the exact pattern
- Port your simplest feature page first (e.g., a text editor, settings viewer) to validate the full stack: Model → Page → Route → Navigation
- Port shared utilities: string helpers, extension methods, converters — anything with no UI or platform dependency
- Port dialogs: WPF child
Windowinstances becomeContentDialog
Phase 3: Service Abstraction (~2,000 LOC)
- Define interfaces for every platform-specific service (e.g.,
IOcrEngine,IFileService,IClipboardService) - Register platform-specific implementations with
#if WINDOWSguards inApp.xaml.cs:#if WINDOWS services.AddSingleton<IOcrEngine, WindowsOcrEngine>(); #endif - Create service facades that route to the correct implementation based on runtime context
- Replace all
Singleton<T>.Instancecalls with constructor-injected interfaces
Phase 4: Remaining Pages (~2,000-3,000 LOC per page)
Port pages in order of dependency, simplest first. For each page:
- Create an MVUX Model with
IState<T>for mutable data,IFeed<T>for read-only - Create the XAML Page referencing the Model
- Register the route in
App.xaml.cs - Build and test on at least two targets (Windows + one other)
Phase 5: Settings and Theme (~1,700 LOC)
- Migrate settings:
Properties.Settings.Default→IWritableOptions<AppSettings>withSection<AppSettings>()inUseConfiguration. See references/04-settings-theme-services.md - Migrate theme switching:
App.SetTheme()→SystemThemeHelper.SetApplicationTheme() - Implement nested NavigationView for settings sub-pages (same Frame pattern as main Shell)
Phase 6: Platform Polish (~2,200 LOC)
- First-run flow: Use
ContentDialogshown inShellPage.Loaded, not route-based navigation - In-app notifications:
InfoBaroverlay in ShellPage, replacing Windows toast notifications - History/storage:
ApplicationData.Current.LocalFolderfor cross-platform file storage - Platform guards:
#if WINDOWSin code-behind for Windows-only features;Visibility="Collapsed"in XAML with code-behind toggle - Screen capture: Abstract behind
IScreenCaptureService; Windows uses P/Invoke GDI BitBlt + SkiaSharp; other platforms fall back to file picker or clipboard - Global hotkeys: Abstract behind
IHotKeyService; Windows uses P/InvokeRegisterHotKey/WM_HOTKEY; non-Windows hides the UI
Phase 7: Tests and Gap Closure (~1,700 LOC)
- Port portable unit tests first — string utilities, models, service logic (no UI dependency)
- Add MVUX model tests for reactive state behavior
- Systematic gap closure: compare WPF UI element-by-element (every
MenuFlyoutItem, every toolbar button) against Uno to find missing commands - Track parity percentage per feature area, not just per phase — prevents "scaffolded but not functional" false completions
Phase 8: Validate
- Build all targets:
dotnet buildfor each TFM - Test navigation: every route renders content (not blank)
- Test settings: values persist across app restart
- Test on non-Windows target to catch platform assumptions
Critical Patterns
NavigationView Region Navigation
This is the #1 source of blank-page bugs. Must use Frame, not Grid/Panel with Visibility navigator.
<!-- CORRECT: Frame inside NavigationView -->
<NavigationView uen:Region.Attached="true">
<NavigationView.MenuItems>
<NavigationViewItem uen:Region.Name="PageA" Content="Page A" />
<NavigationViewItem uen:Region.Name="PageB" Content="Page B" />
</NavigationView.MenuItems>
<Frame uen:Region.Attached="true" />
</NavigationView>
<!-- WRONG: Visibility navigator inside NavigationView — renders blank -->
<Grid uen:Region.Attached="true" uen:Region.Navigator="Visibility">
<Grid uen:Region.Name="PageA" />
</Grid>
Key rules:
IsDefault: trueon the Shell route is required for automatic startup navigation- Nested NavigationView (e.g., Settings sub-pages) uses the exact same Frame pattern
- Visibility navigator only works outside NavigationView (e.g., TabBar, Panel)
Material Theme Resource Keys
Wrong resource keys cause silent runtime blank pages — no compile error.
| Category | Correct (Uno Material) | Wrong (WPF/WinUI) |
|---|---|---|
| Typography | TitleLarge, BodyMedium |
TitleLargeTextBlockStyle |
| Background | SurfaceBrush, BackgroundBrush |
ApplicationPageBackgroundThemeBrush |
| Status | Use ErrorBrush or custom |
SystemFillColorCautionBrush |
Pattern: {Category}{Size} — DisplayLarge, HeadlineMedium, TitleSmall, BodyMedium, LabelSmall
Namespace Collision with global::
If your project namespace starts with a common prefix (e.g., MyApp.Uno), it can shadow Uno.* namespaces:
// ERROR: Resolves to MyApp.Uno.Extensions.Configuration instead of Uno.Extensions.Configuration
IWritableOptions<AppSettings> settings;
// FIX: Use global:: prefix
global::Uno.Extensions.Configuration.IWritableOptions<AppSettings> settings;
global::Uno.Toolkit.UI.SystemThemeHelper.SetApplicationTheme(xamlRoot, theme);
WPF Window to ContentDialog
Every WPF child window becomes a ContentDialog:
| WPF | Uno Platform |
|---|---|
new MyDialog().ShowDialog() |
new MyDialog().ShowAsync() |
DialogResult = true |
ContentDialogResult.Primary |
Owner = this |
XamlRoot = this.XamlRoot |
Advanced patterns:
- Simple forms: build UI programmatically in code-behind (
StackPanel+TextBoxes) - Complex dialogs: dedicated XAML
ContentDialogsubclass inDialogs/folder - Nested dialogs: a ContentDialog can show another ContentDialog (e.g., manager → delete confirmation)
- Keep dialog open:
SecondaryButtonhandler withargs.Cancel = true(useful for "Reset Defaults")
WPF RoutedCommand → KeyboardAccelerators
WPF's RoutedCommand + CommandBindings system has no equivalent in WinUI:
| WPF | Uno Platform |
|---|---|
RoutedCommand + CommandBinding |
Direct Click handler calling the same method |
ApplicationCommands.Undo/Redo |
Custom undo stack (see canvas undo pattern in ref/05) |
InputGestureText (display only) |
KeyboardAccelerators (functional — actually fires the command) |
Pattern: each MenuFlyoutItem gets both a Click handler and a KeyboardAccelerator that call the same method.
Settings Migration Pattern
// WPF: Properties.Settings.Default.MyValue = x;
// Uno: IWritableOptions<AppSettings> with MVUX
public partial record SettingsModel(IWritableOptions<AppSettings> Settings)
{
public IState<bool> IsFeatureEnabled => State.Async(async ct =>
{
var s = await Settings.GetAsync(ct);
return s.IsFeatureEnabled;
});
public async ValueTask ToggleFeature(bool value) =>
await Settings.UpdateAsync(s => s with { IsFeatureEnabled = value });
}
Section<AppSettings>() in UseConfiguration registers both IOptions<AppSettings> and IWritableOptions<AppSettings> — no extra registration needed.
Common Mistakes
Architecture:
- Replicating WPF's multi-window model instead of adopting single-window + Page navigation
- Keeping static singletons instead of migrating to DI (breaks MVUX, testability, and cross-platform)
- Porting WPF code-behind event handlers verbatim instead of using MVUX reactive patterns
- Using route-based navigation for first-run flows (ContentDialog is simpler and avoids back-stack issues)
- Marking migration phases "complete" when features are scaffolded but not functional — track parity per feature area
XAML:
- Using long WinUI-style typography keys (
TitleLargeTextBlockStyle) instead of Material short keys (TitleLarge) - Referencing WPF/WinUI system brushes that don't exist in Material theme (causes silent blank pages)
- Using
{Binding StringFormat=...}which is not supported — use<Run>elements or computed properties - Setting
Page.Backgroundexplicitly instead of inheriting from parent theme
Navigation:
- Using Visibility navigator inside NavigationView (renders blank — must use Frame)
- Forgetting
IsDefault: trueon the Shell route (app launches to blank screen) - Not adding
uen:Region.Attached="true"to both NavigationView AND its child Frame
Platform:
- Using
#if WINDOWSin XAML (not supported) — use Visibility + code-behind toggle instead - Passing
SoftwareBitmapacross service boundaries (not cross-platform) — useStreamorbyte[] - Subclassing
Buttonas XAML root element (not supported in WinUI) — useUserControlwrapper - Forgetting
global::prefix when project namespace shadowsUno.*
Build:
- Stale XAML compiler cache causing
XamlCompiler.exeexit code 1 — rundotnet cleanor deleteobj/ - Not running
uno-checkbefore starting migration - Not building all TFMs regularly during migration (platform bugs compound)
Related Skills
| Skill | Use instead when... |
|---|---|
uno-migration-troubleshoot |
Migrating from Silverlight, upgrading .NET versions, or fixing build errors (UNOB0011, UNOB0013) |
uno-platform-agent |
Creating a new Uno project from scratch (not migrating an existing WPF app) |
uno-navigation |
Setting up navigation or NavigationView in a new project (this skill covers migration-specific navigation patterns) |
uno-extensions-services |
Configuring DI, auth, HTTP, or logging (not migration-specific) |
uno-material |
Installing Material theme from scratch (this skill covers migrating WPF theme patterns) |
winui-xaml |
XAML layout, binding, and performance best practices (not migration-specific) |
uno-toolkit |
Using Toolkit controls (AutoLayout, SafeArea, TabBar) after migration |
Detailed References
Read the reference file matching your current migration phase:
- references/01-namespace-api-mapping.md — Read when doing the initial namespace sweep and API mapping pass
- references/02-xaml-patterns-and-controls.md — Read when replacing unsupported XAML features, mapping WPF controls, or debugging blank pages from bad resource keys
- references/03-architecture-and-navigation.md — Read when setting up Shell, NavigationView, route registration, or nested navigation
- references/04-settings-theme-services.md — Read when migrating settings, theme switching, or abstracting platform-specific services with DI
- references/05-platform-specific-gotchas.md — Read when handling Windows-only features, ContentDialog conversions, first-run flows, screen capture, global hotkeys, undo/redo, or runtime gotchas