Migration Navigation
Key Takeaways
  • 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.cs via ViewMap, DataViewMap, and RouteMap
  • The Qualifiers class 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 Single Shell

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.

Decision Tree

The Three-Way Decision: Page, ContentDialog, or Secondary Window

Not every Window should become a Page. Use this decision table before porting:

WPF SourceUno TargetWhy
Window shown by Show() that represents a top-level section (Home, Orders, Reports)Page hosted in NavigationViewThis is exactly the scenario NavigationView is designed for
Window.ShowDialog() with OK/Cancel semantics returning a DialogResultContentDialog navigated with the ! qualifierShowDialog 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 onlySupported per Uno Platform Windowing, but not on Android/iOS
Pop-up picker (file/color/date)Built-in flyout or ContentDialogDo not re-implement; use the built-in Fluent patterns
Window that hosts wizard stepsSingle Page with a ContentControl regionRegion-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.

NavigationView

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

App.xaml.cs
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

XAML
<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>
ShowDialog

Replacing Window.ShowDialog() with ContentDialog

The WPF pattern:

WPF
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:

Uno Platform (C#)
// Modal ContentDialog via navigation
_ = this.Navigator()?.NavigateViewAsync<AboutDialog>(
    this, qualifier: Qualifiers.Dialog);

From XAML:

Uno Platform (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.

Secondary Window

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.

Desktop Only
#if HAS_UNO || WINDOWS
var preview = new Window();
preview.Content = new PreviewPage();
preview.Activate();
#endif

Two 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.

Limitations

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.
Checklist

Migration Checklist: 8 Questions Before Porting Each Window

  1. Is this window ever shown with ShowDialog()? If yes, it becomes a ContentDialog with the ! qualifier.
  2. Does the window return a DialogResult that gates the caller? If yes, ContentDialog with PrimaryButtonText/SecondaryButtonText preserves the contract.
  3. Does the window represent a top-level section of the app? If yes, it becomes a Page under NavigationView.
  4. Is the window a wizard or multi-step flow? If yes, consider a single Page with a ContentControl region.
  5. Must the window be detachable on desktop? If yes, it can remain a secondary Window, but guard mobile targets.
  6. Does the window need deep-linking or back-stack management? Plan qualifier usage up front per the Navigation Qualifiers reference.
  7. Does the window pass data to its ViewModel on open? Use DataViewMap and uen:Navigation.Data.
  8. Is the window purely cosmetic (splash, about)? ContentDialog or an ExtendedSplashScreen replaces it.
FAQ

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.

Next Step

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.