- WPF
Window.ShowDialog()maps to a ContentDialog navigated with the!qualifier, not a new Window - Uno Platform's NavigationView walkthrough uses region-based navigation via
uen:Region.Attached="True"on the content host - Secondary windows are supported on desktop targets only; opening a second Window on Android or iOS throws an
InvalidOperationException - Route registration goes in
App.xaml.csviaViewMap,DataViewMap, andRouteMap - The
Qualifiersclass exposes prefixes such as!(dialog) and-/(clear back stack)
Migrating a WPF multi-window app to a NavigationView shell is less about rewriting XAML and more about deciding which Window instances deserve to stay as windows. Most WPF apps that accumulated ten or twenty top-level Window objects end up collapsing to a single NavigationView, a handful of ContentDialog modals, and at most one or two legitimate secondary windows on desktop.
This guide is the decision tree plus the region-navigation wiring that most WinUI tutorials skip.
Why WPF Apps Accumulate Windows, and Why Uno Platform Pushes a Single Shell
In WPF, Window.Show() opens a modeless window that users interact with independently, while Window.ShowDialog() opens a modal that restricts interaction until it closes. Because both are cheap to instantiate and the StartupUri pattern trains developers to think in windows, WPF apps frequently grow a top-level Window for every settings screen, wizard step, tool palette, and preferences dialog.
Uno Platform targets desktop, mobile, and WebAssembly from a single codebase. Mobile platforms do not support multiple top-level windows: creating a second Window on Android or iOS throws an InvalidOperationException. That constraint, combined with WinUI's recommendation to use NavigationView as an adaptive top-level container, pushes cross-platform apps toward a single-shell model.
The practical consequence: every WPF Window has to be reclassified before it is ported.
The Three-Way Decision: Page, ContentDialog, or Secondary Window
Not every Window should become a Page. Use this decision table before porting:
| WPF Source | Uno Target | Why |
|---|---|---|
Window shown by Show() that represents a top-level section (Home, Orders, Reports) | Page hosted in NavigationView | This is exactly the scenario NavigationView is designed for |
Window.ShowDialog() with OK/Cancel semantics returning a DialogResult | ContentDialog navigated with the ! qualifier | ShowDialog is a modal pattern; ContentDialog covers the same contract |
Window.Show() used for a detached, persistent surface (preview, inspector, tool palette) | Secondary Window on desktop targets only | Supported per Uno Platform Windowing, but not on Android/iOS |
| Pop-up picker (file/color/date) | Built-in flyout or ContentDialog | Do not re-implement; use the built-in Fluent patterns |
| Window that hosts wizard steps | Single Page with a ContentControl region | Region-based navigation within a placeholder |
Apply this table once per WPF Window before touching XAML. In practice the majority of Window subclasses in a typical line-of-business WPF app are rows two and four, which means they never needed to be windows in the first place.
Setting Up NavigationView with Region-Based Navigation
You register views and routes in App.xaml.cs via IViewRegistry and IRouteRegistry, attach a region to the content host in XAML, and the Navigation extensions resolve NavigationViewItem selections to the right Page.
Register Views and Routes
private static void RegisterRoutes(
IViewRegistry views, IRouteRegistry routes)
{
views.Register(
new ViewMap(ViewModel: typeof(ShellViewModel)),
new ViewMap<MainPage, MainViewModel>(),
new ViewMap<ProductsPage, ProductsViewModel>(),
new ViewMap<SettingsPage, SettingsViewModel>(),
new ViewMap<AboutDialog, AboutViewModel>()
);
routes.Register(
new RouteMap("", View: views.FindByViewModel<ShellViewModel>(),
Nested: [
new RouteMap("Main", View: views.FindByViewModel<MainViewModel>(),
Nested: [
new RouteMap("Products", View: views.FindByViewModel<ProductsViewModel>()),
new RouteMap("Settings", View: views.FindByViewModel<SettingsViewModel>()),
new RouteMap("About", View: views.FindByViewModel<AboutViewModel>())
])
])
);
}Nest NavigationViewItem routes under Main to update only the content region, not the entire page.
XAML: Attach the Region
<Page xmlns:uen="using:Uno.Extensions.Navigation.UI">
<NavigationView>
<NavigationView.MenuItems>
<NavigationViewItem Content="Products"
uen:Region.Name="Products" />
<NavigationViewItem Content="Settings"
uen:Region.Name="Settings" />
</NavigationView.MenuItems>
<Grid uen:Region.Attached="True">
<!-- Selected page is hosted here -->
</Grid>
</NavigationView>
</Page>Replacing Window.ShowDialog() with ContentDialog
The WPF pattern:
var dialog = new SettingsWindow();
dialog.Owner = this;
var result = dialog.ShowDialog();
if (result == true)
{
// apply settings
}In Uno Platform, route that through INavigator with the dialog qualifier. From code:
// Modal ContentDialog via navigation
_ = this.Navigator()?.NavigateViewAsync<AboutDialog>(
this, qualifier: Qualifiers.Dialog);From XAML:
<Button Content="About"
uen:Navigation.Request="!About" />The ! prefix indicates dialog navigation. Whether the target is a Page (flyout) or a ContentDialog (modal) is determined by the target type itself. This is the replacement for the vast majority of WPF secondary Window classes. It keeps modal semantics, runs on every Uno Platform target including WebAssembly and mobile, and avoids the InvalidOperationException that a second native Window would raise on phones.
Keeping a Legitimate Secondary Window
Some windows genuinely are windows: a detached preview pane a user drags to a second monitor, a floating inspector that stays visible while the main shell scrolls.
#if HAS_UNO || WINDOWS
var preview = new Window();
preview.Content = new PreviewPage();
preview.Activate();
#endifTwo constraints: Mobile targets reject secondary windows (creating a second Window on Android or iOS throws an InvalidOperationException). WinUI currently does not provide a way to enumerate open windows of an application; track windows manually.
A defensible rule: aim for at most two Window instances in a migrated desktop app, and only if the UX actually requires detached presentation. Everything else is a Page or ContentDialog.
When This Approach Does Not Apply
The NavigationView-plus-ContentDialog model covers the vast majority of WPF migrations, but three cases need flagging up front:
- MDI-style apps. WPF apps built around a true Multiple Document Interface (for example, a CAD tool with many child document windows docked inside a parent) do not collapse cleanly into a NavigationView. Those apps need a tabbed-document shell (
TabView) or a docking-library equivalent. - Per-monitor DPI on secondary windows. When you keep a legitimate secondary Window on desktop, per-monitor DPI awareness on WinAppSDK and the Skia desktop backend is not identical to WPF's auto-scaling behavior. Test DPI handoff manually if the user drags the detached window between monitors.
- WASM URL synchronization with qualifier routes. How Uno.Extensions.Navigation qualifier prefixes (
!,-/) map to browser URL paths on WebAssembly is not explicitly documented. If deep-link URL fidelity on the web is a hard requirement, validate behavior on a prototype before committing.
Migration Checklist: 8 Questions Before Porting Each Window
- Is this window ever shown with ShowDialog()? If yes, it becomes a ContentDialog with the
!qualifier. - Does the window return a DialogResult that gates the caller? If yes, ContentDialog with
PrimaryButtonText/SecondaryButtonTextpreserves the contract. - Does the window represent a top-level section of the app? If yes, it becomes a Page under NavigationView.
- Is the window a wizard or multi-step flow? If yes, consider a single Page with a
ContentControlregion. - Must the window be detachable on desktop? If yes, it can remain a secondary Window, but guard mobile targets.
- Does the window need deep-linking or back-stack management? Plan qualifier usage up front per the Navigation Qualifiers reference.
- Does the window pass data to its ViewModel on open? Use
DataViewMapanduen:Navigation.Data. - Is the window purely cosmetic (splash, about)? ContentDialog or an ExtendedSplashScreen replaces it.
FAQ
Do I need a separate Window for a tool palette?
On desktop only, and only if the palette is genuinely detachable. For docked palettes use a SplitView or a region inside the main shell.
Does NavigationView work on WebAssembly and mobile?
Yes. Uno Platform lists NavigationView as implemented on WASM, Skia, and Mobile, including its ItemInvoked and DisplayModeChanged events.
Can I keep Window.ShowDialog behavior with a ContentDialog?
Yes. A ContentDialog opened via the ! qualifier is modal by definition; awaiting the NavigateViewAsync call lets you act on the result, matching WPF's ShowDialog return contract.
What happens to my App.StartupUri pattern?
It goes away. Uno Platform apps bootstrap through the host builder and start at a shell view. The RegisterRoutes walkthrough is the replacement entry point.
Can I use AI/Claude Code to automate this reclassification?
Yes. The Uno Platform Claude Code setup guide explains how to wire the Uno MCP servers so an agent can read each WPF Window class and propose a Page-vs-ContentDialog-vs-secondary-Window classification backed by the correct Navigation walkthrough citation.
See the NavigationView region-based navigation walkthrough for the full end-to-end wiring, and the Uno Platform WPF Migration hub for the broader campaign.
- WPF Windows Overview (Microsoft Learn) →
- WPF Window.ShowDialog API →
- WinUI NavigationView Control →
- Uno Platform NavigationView Walkthrough →
- Uno Platform Show Dialog Walkthrough →
- Uno Platform Register Routes →
- Uno Platform Define Routes →
- Advanced Page Navigation →
- Navigation Qualifiers Reference →
- UseContentControl Walkthrough →
- Uno Platform Windowing Feature →
- Uno Platform Dialogs Feature →
- Get Started with Claude + Uno Platform →
- Uno Platform MCP Servers →
- Uno Platform for WPF Developers →
Subscribe to Our Blog
Subscribe via RSS
Back to Top