Windows, Dialogs & Navigation

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.

Almost every desktop app is a set of windows that open, talk to each other and close. This page covers the pieces WPF gives you for that: the Window class and the events of its lifetime, owned and modal windows, the dialog boxes you can write yourself or borrow from Windows (MessageBox and the common file and folder dialogs), and the browser-style navigation model built from Page, Frame, NavigationWindow and NavigationService. It was written against .NET 10 LTS (latest patch 10.0.13) with C# 14, Visual Studio 2026 18.11.1, Windows App SDK 2.5.1 and CommunityToolkit.Mvvm 8.4.2 on Windows 10 and Windows 11. .NET 11 is at release candidate 2 and general availability is expected at .NET Conf, 10 to 13 November 2026; nothing on this page changes for it as far as the published release notes show. WPF is the stack in focus; WinUI 3 and Windows Forms are compared in a table near the end.

The running scenario is Bookshelf Desktop. A MainWindow shows the book list from BooksViewModel; an edit dialog bound to a BookDetailsViewModel is opened modally through an IDialogService; a MessageBox asks for confirmation before a book is deleted; the common dialogs import and export the library; and a Frame hosts the settings pages through a small page-local IFrameNavigator helper. The application object (App.xaml, StartupUri, ShutdownMode) is covered on the WPF Architecture & Application Model page, and the commands and view-model patterns used in the examples on the Commands and Data Binding pages of this section. The XAML syntax itself is on XAML Language Fundamentals.

The Window class

A WPF window is an instance of System.Windows.Window, a content control with a non-client area drawn by Windows (title bar, icon, minimize, maximize and close buttons, system menu, border) and a client area that you fill. A window is normally written as XAML plus a code-behind partial class. The x:Class attribute makes the build generate the other half of the class, including InitializeComponent, which loads the markup (see Project Structure & Build for the generated files).

The properties you set most often:

Property Values Purpose

Title, Icon

string, ImageSource

Caption and icon of the non-client area and the taskbar button.

Width, Height, MinWidth, MaxWidth, MinHeight, MaxHeight

double

The allowed size range. ActualWidth and ActualHeight hold the real size.

SizeToContent

Manual, Width, Height, WidthAndHeight

Fit the window to its content when it is first shown. Min/Max values win over it.

WindowStartupLocation

Manual, CenterScreen, CenterOwner

Where the window first appears. With Manual and no Left/Top, Windows chooses.

WindowState

Normal, Minimized, Maximized

Current state. Left, Top, Width and Height always describe the normal state.

WindowStyle

None, SingleBorderWindow, ThreeDBorderWindow, ToolWindow

The border and caption buttons. None plus AllowsTransparency gives non-rectangular windows.

ResizeMode

NoResize, CanMinimize, CanResize, CanResizeWithGrip

Whether and how the user can resize the window.

ShowInTaskbar, Topmost

bool

Taskbar button; topmost z-order.

ShowActivated

bool

Set to false before the first Show so the window appears without taking focus (a chat popup, a notification).

Owner, OwnedWindows

Window, collection

The ownership relationship described below.

DialogResult

bool?

Only valid for a window shown with ShowDialog; setting it closes the window.

The Learn page lists CenterOwner as the default of WindowStartupLocation; the Window API reference documents the default as Manual. Set the value explicitly in every dialog so the question never matters.

Window lifetime

A window is created with new, opened with Show or ShowDialog, activated and deactivated as the user moves around, and closed. A closed window cannot be reopened: create a new instance instead. Every Window that is instantiated is added to Application.Windows, and the first one becomes Application.MainWindow.

Event When it is raised

SourceInitialized

The first time the window is shown, once its HwndSource exists. This is the earliest moment a window handle is available, for example through WindowInteropHelper.

Activated

The window becomes the foreground window of the app. Raised before Loaded and ContentRendered on the first open.

Loaded (from FrameworkElement)

The element tree is built, laid out and ready for rendering.

ContentRendered

The content has been rendered; the window can be considered opened. Use it for work that must wait until the first paint.

Deactivated

The user switched to another window or app. IsActive becomes false.

Closing

Raised before the window closes. The handler receives CancelEventArgs; set Cancel to true to keep the window open.

Closed

The window is closing for good and can no longer be stopped. Release subscriptions here.

The diagram shows the states a window goes through and the events that move it between them.

stateDiagram-v2 [*] --> Constructed: new Window Constructed --> Initialized: Show or ShowDialog, SourceInitialized Initialized --> Active: Activated Active --> Opened: Loaded then ContentRendered Opened --> Inactive: Deactivated Inactive --> Opened: Activated Opened --> Closing: Close, Alt+F4, Owner closes, Shutdown Inactive --> Closing: Close Closing --> Opened: Closing with Cancel = true Closing --> Closed: Closed event Closed --> [*]

When a window is shown with ShowActivated="False", the Activated event is not raised and the previously active window stays active until the user clicks the new one. A window can also be closed implicitly: when the user logs off or shuts Windows down, when its Owner closes, when the main window closes and ShutdownMode is OnMainWindowClose, and when Application.Shutdown is called.

Cancelling a close

Closing is the place to protect unsaved work. The following handler asks BooksViewModel whether the user has pending edits and, only if so, asks for confirmation.

using System.ComponentModel;
using System.Windows;

namespace Bookshelf.Desktop;

public partial class MainWindow : Window
{
    private readonly BooksViewModel _viewModel;

    public MainWindow(BooksViewModel viewModel)
    {
        InitializeComponent();
        _viewModel = viewModel;
        DataContext = viewModel;
    }

    protected override void OnClosing(CancelEventArgs e)
    {
        if (_viewModel.HasUnsavedChanges)
        {
            var answer = MessageBox.Show(
                this,
                "There are unsaved changes. Close without saving?",
                "Bookshelf Desktop",
                MessageBoxButton.YesNo,
                MessageBoxImage.Warning,
                MessageBoxResult.No);

            if (answer == MessageBoxResult.No)
            {
                e.Cancel = true;
            }
        }

        base.OnClosing(e);
    }
}

Source: Windows in WPF overview (the "Cancel window closure" section)

Overriding OnClosing is equivalent to handling the Closing event and keeps the logic in the class. Do not show a long-running or asynchronous operation from Closing: the event is synchronous, and an await inside an async void handler returns to the caller before the decision is made, so the window closes anyway. Save before closing or cancel, set the flag, and call Close again from the continuation.

Showing windows: modeless, modal and owned

Window.Show opens a modeless window: it returns immediately and the user can interact with every window of the application. Window.ShowDialog opens a modal window: the call does not return until the window closes, and the other windows of the same application (not of the whole desktop) ignore input. While ShowDialog is waiting, WPF keeps pumping messages, so timers, bindings and Dispatcher work continue to run, which is a source of re-entrancy bugs (see the pitfalls below).

Show (modeless) ShowDialog (modal)

Returns

Immediately, void

When the window closes, with bool?

Other windows of the app

Stay interactive

Blocked until it closes

Result

A property on the window, an event, or Closed

ShowDialog() return value and DialogResult

Typical use

Tool windows, find/replace, secondary documents

Edit forms, confirmations, settings, errors

A window opened with Show has no relationship with the window that created it: either can cover the other and be minimized without affecting it. To tie them together, set Owner before showing. An owned window always stays above its owner, is minimized, restored and closed with it, and does not get a taskbar button when ShowInTaskbar is false. OwnedWindows on the owner lists them. For ShowDialog the owner also decides which windows are disabled, so a dialog without an owner can appear behind the main window or be reachable from the taskbar in a confusing way: always set Owner. WindowStartupLocation="CenterOwner" then centres the dialog over it.

A custom dialog: the book editor

A dialog is a window with a particular intent. The Learn guidance, which the following XAML follows, is to give it an OK button with IsDefault="True" (Enter accepts), a Cancel button with IsCancel="True" (Esc cancels), a title that says what it does, a minimum size, and to turn off resizing and the taskbar button when the content is fixed.

<Window x:Class="Bookshelf.Desktop.BookDetailsWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Edit book"
        MinWidth="360"
        SizeToContent="WidthAndHeight"
        ResizeMode="NoResize"
        ShowInTaskbar="False"
        WindowStartupLocation="CenterOwner"
        FocusManager.FocusedElement="{Binding ElementName=TitleBox}">
    <Grid Margin="16">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Label Grid.Row="0" Content="_Title:" Target="{Binding ElementName=TitleBox}"/>
        <TextBox x:Name="TitleBox" Grid.Row="0" Grid.Column="1" Margin="0,0,0,8"
                 Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}"/>

        <Label Grid.Row="1" Content="_Author:" Target="{Binding ElementName=AuthorBox}"/>
        <TextBox x:Name="AuthorBox" Grid.Row="1" Grid.Column="1" Margin="0,0,0,16"
                 Text="{Binding Author, UpdateSourceTrigger=PropertyChanged}"/>

        <StackPanel Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal"
                    HorizontalAlignment="Right">
            <Button Content="_Save" Width="80" Margin="0,0,8,0" IsDefault="True"
                    Click="OnSaveClicked"/>
            <Button Content="Cancel" Width="80" IsCancel="True"/>
        </StackPanel>
    </Grid>
</Window>
using System.Windows;

namespace Bookshelf.Desktop;

public partial class BookDetailsWindow : Window
{
    public BookDetailsWindow(BookDetailsViewModel viewModel)
    {
        InitializeComponent();
        DataContext = viewModel;
    }

    private void OnSaveClicked(object sender, RoutedEventArgs e)
    {
        if (DataContext is BookDetailsViewModel { CanSave: true })
        {
            // Setting DialogResult closes a window shown with ShowDialog.
            DialogResult = true;
        }
    }
}

The cancel button needs no handler: when a window is opened with ShowDialog, pressing Esc on a button whose IsCancel is true closes the window and sets DialogResult to false. Setting DialogResult on a window that was opened with Show throws InvalidOperationException; close such a window with Close.

Keeping windows out of the view model: IDialogService

A view model must not create windows: it would then depend on WPF and could not be unit-tested. The usual solution is an interface the view model depends on, with a WPF implementation registered in the Generic Host. There is no official dialog service in WPF or in CommunityToolkit.Mvvm; the following is a user pattern built only from Window, Owner and ShowDialog. The Navigation, Dialogs & Services in MVVM page covers the pattern in depth; here is the minimum that the examples on this page use.

namespace Bookshelf.Core;

public interface IDialogService
{
    /// <summary>Asks a yes/no question and returns the answer.</summary>
    Task<bool> ConfirmAsync(string title, string message);

    /// <summary>Opens the add/edit dialog modally; returns null when the user cancels.</summary>
    Task<Book?> EditBookAsync(Book? existing);
}

The two members are asynchronous so that the same interface can be implemented in WPF, WinUI 3 (where a ContentDialog is awaited) and in tests. A synchronous bool? ShowBookDetails(...) / bool Confirm(...) pair works in WPF alone, but it cannot be implemented over a ContentDialog, which is why the section uses the asynchronous shape everywhere.

using System.Linq;
using System.Windows;
using Bookshelf.Core;
using Bookshelf.Desktop.ViewModels;
using Microsoft.Extensions.DependencyInjection;

namespace Bookshelf.Desktop;

public sealed class WpfDialogService(IServiceProvider services) : IDialogService
{
    public Task<Book?> EditBookAsync(Book? existing)
    {
        var details = new BookDetailsViewModel(existing);
        var window = ActivatorUtilities.CreateInstance<BookDetailsWindow>(services, details);
        window.Owner = GetActiveWindow();
        return Task.FromResult(window.ShowDialog() == true ? details.ToBook() : null);
    }

    public Task<bool> ConfirmAsync(string title, string message) =>
        Task.FromResult(
            MessageBox.Show(GetActiveWindow(), message, title,
                MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No)
            == MessageBoxResult.Yes);

    // The active window is the natural owner; fall back to the main window.
    private static Window? GetActiveWindow() =>
        Application.Current.Windows.OfType<Window>().FirstOrDefault(w => w.IsActive)
        ?? Application.Current.MainWindow;
}
using System.Collections.ObjectModel;
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace Bookshelf.Desktop.ViewModels;

public sealed partial class BooksViewModel(IBookRepository repository, IDialogService dialogs)
    : ObservableObject
{
    public ObservableCollection<BookViewModel> Books { get; } = [];

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(EditSelectedCommand))]
    [NotifyCanExecuteChangedFor(nameof(DeleteSelectedCommand))]
    public partial BookViewModel? SelectedBook { get; set; }

    [RelayCommand(CanExecute = nameof(HasSelection))]
    private async Task EditSelectedAsync()
    {
        if (SelectedBook is null) return;

        var edited = await dialogs.EditBookAsync(SelectedBook.Model);
        if (edited is null) return;

        await repository.UpdateAsync(edited, CancellationToken.None);
        // BookViewModel wraps an immutable Book, so the wrapper is replaced.
        Books[Books.IndexOf(SelectedBook)] = new BookViewModel(edited);
    }

    [RelayCommand(CanExecute = nameof(HasSelection))]
    private async Task DeleteSelectedAsync()
    {
        if (SelectedBook is null) return;
        if (!await dialogs.ConfirmAsync("Bookshelf Desktop", $"Delete \"{SelectedBook.Title}\"?")) return;

        await repository.DeleteAsync(SelectedBook.Id, CancellationToken.None);
        Books.Remove(SelectedBook);
    }

    private bool HasSelection() => SelectedBook is not null;
}

BookDetailsViewModel (its constructor takes the Book to edit, or null for a new one, and ToBook produces the edited Book) is the dialog view model of this page and is illustrative; Book, BookViewModel, IBookRepository and IDialogService are the canonical Bookshelf Desktop types used throughout this section. The service is registered with services.AddSingleton<IDialogService, WpfDialogService>() in the host that replaces StartupUri.

MessageBox

System.Windows.MessageBox is the simplest dialog: a static Show with a message, an optional caption, a button set, an icon, a default result and an optional owner window. It returns a MessageBoxResult. It is modal, it is drawn by the operating system, and it blocks the calling thread while pumping messages.

Parameter Notes

owner (Window)

Pass it. Without an owner the box is not tied to your window and may appear behind it.

messageBoxText, caption

Plain text. No markup, but \n line breaks work.

button (MessageBoxButton)

The set of buttons; see the next table.

icon (MessageBoxImage)

None, Information, Warning, Error, Question, Hand, Stop, Exclamation, Asterisk; each maps to a system sound too.

defaultResult (MessageBoxResult)

Which button has focus. Make the safe choice the default for destructive questions (No, Cancel).

options (MessageBoxOptions)

RightAlign, RtlReading, ServiceNotification, DefaultDesktopOnly.

What is new in .NET 10. The WPF release notes for .NET 10 add button sets and results to MessageBox, contributed by the community. Before .NET 10 only OK, OKCancel, YesNoCancel and YesNo existed; the enums now read as follows.

MessageBoxButton Value Buttons shown

OK

0

OK

OKCancel

1

OK, Cancel

AbortRetryIgnore

2

Abort, Retry, Ignore (new in .NET 10)

YesNoCancel

3

Yes, No, Cancel

YesNo

4

Yes, No

RetryCancel

5

Retry, Cancel (new in .NET 10)

CancelTryContinue

6

Cancel, Try Again, Continue (new in .NET 10)

MessageBoxResult gains Abort (3), Retry (4), Ignore (5), TryAgain (10) and Continue (11) next to the existing None (0), OK (1), Cancel (2), Yes (6) and No (7).

A realistic use in Bookshelf Desktop is a failed import that can be retried:

using System.Windows;

namespace Bookshelf.Desktop;

public static class ImportErrors
{
    /// <summary>Returns true when the caller should retry the import.</summary>
    public static bool AskRetry(Window owner, string path, Exception error)
    {
        var result = MessageBox.Show(
            owner,
            $"Could not import \"{path}\":\n{error.Message}",
            "Import failed",
            MessageBoxButton.RetryCancel,   // new in .NET 10
            MessageBoxImage.Error,
            MessageBoxResult.Cancel);

        return result == MessageBoxResult.Retry;
    }
}

On earlier target frameworks MessageBoxButton.RetryCancel does not exist, so code that uses the new values only compiles for net10.0-windows and later. A message box cannot be styled, localized per button or hold arbitrary content; when you need that, write a custom dialog as above.

Common dialogs

Windows provides reusable dialogs for choosing files and folders and for printing. WPF wraps them in managed classes that return bool? from ShowDialog, like your own dialogs.

Class Namespace Main members

OpenFileDialog

Microsoft.Win32

Filter, FilterIndex, DefaultExt, FileName, FileNames, Multiselect, InitialDirectory, Title, CheckFileExists

SaveFileDialog

Microsoft.Win32

FileName, DefaultExt, Filter, OverwritePrompt, AddExtension, InitialDirectory

OpenFolderDialog (.NET 8 and later)

Microsoft.Win32

FolderName, FolderNames, SafeFolderName, SafeFolderNames, Multiselect, Title, InitialDirectory

PrintDialog

System.Windows.Controls

PageRangeSelection, UserPageRangeEnabled, PrintQueue, PrintTicket (see the Documents, Printing & Text page)

The filter string uses the Win32 format description|pattern|description|pattern. All the file dialogs accept an owner: dialog.ShowDialog(owner). Pass one so the dialog is modal to the right window.

Because dialogs are UI, keep them behind a service just like message boxes. This is the file-picking part of Bookshelf Desktop, with the folder dialog used to choose a backup location:

using Microsoft.Win32;
using System.Windows;

namespace Bookshelf.Desktop;

public interface IFileDialogService
{
    string? PickImportFile();
    string? PickExportFile(string suggestedName);
    string? PickBackupFolder();
}

public sealed class WpfFileDialogService : IFileDialogService
{
    public string? PickImportFile()
    {
        var dialog = new OpenFileDialog
        {
            Title = "Import books",
            Filter = "Bookshelf library (*.json)|*.json|All files (*.*)|*.*",
            CheckFileExists = true,
            Multiselect = false,
        };
        return dialog.ShowDialog(Owner) == true ? dialog.FileName : null;
    }

    public string? PickExportFile(string suggestedName)
    {
        var dialog = new SaveFileDialog
        {
            Title = "Export books",
            FileName = suggestedName,
            DefaultExt = ".json",
            Filter = "Bookshelf library (*.json)|*.json",
            OverwritePrompt = true,
        };
        return dialog.ShowDialog(Owner) == true ? dialog.FileName : null;
    }

    public string? PickBackupFolder()
    {
        var dialog = new OpenFolderDialog
        {
            Title = "Choose the backup folder",
            Multiselect = false,
        };
        return dialog.ShowDialog(Owner) == true ? dialog.FolderName : null;
    }

    private static Window? Owner => Application.Current.MainWindow;
}

Pitfalls specific to the file dialogs: the returned path is user input, so treat it as untrusted (canonicalize it and never concatenate it into a command line); OpenFolderDialog returns an empty string when cancelled, which is why the code checks the bool? first; with Multiselect = true read FileNames / FolderNames rather than the singular property. Windows remembers the last folder per app, so set InitialDirectory only when you really want to override it.

Page-based navigation

WPF also has a browser-style navigation model, inherited from the XBAP era but fully supported in standalone apps on .NET 10. Its parts:

Type Role

Page

Content that can be navigated to: Title, WindowTitle, KeepAlive, and a NavigationService property once hosted.

NavigationWindow

A Window that hosts pages and shows back/forward navigation UI. Its NavigationService is reachable directly.

Frame

A content control that hosts a page inside any other content, with its own NavigationService and optionally its own journal.

NavigationService

Does the work: Navigate, GoBack, GoForward, Refresh, StopLoading, CanGoBack, CanGoForward, plus the navigation events.

PageFunction<T>

A page that behaves like a function: it is called by navigation and returns a typed result.

Hyperlink

NavigateUri navigates the nearest NavigationService; NavigateUri="#section" is fragment navigation.

Pages are navigated to by URI (Source="SettingsPage.xaml", or Navigate(new Uri("SettingsPage.xaml", UriKind.Relative)) with a relative or pack URI) or by object (Navigate(new SettingsPage())). The NavigationService of a page is available as this.NavigationService once it is hosted, or through NavigationService.GetNavigationService(element) for any element inside a page.

The journal

Each navigation host remembers where the user has been in a journal: a back stack and a forward stack. GoBack and GoForward move between entries. By default a journal entry stores metadata, not the page object, so going back creates a new page instance with its parameterless constructor and the state of its controls is lost unless it is saved. There are two ways to keep an instance alive:

  • navigate to an object: Navigate(page) keeps that instance in the journal; or

  • set KeepAlive="True" on the page (the default is False).

Pages that are constructed with arguments (view model injection included) therefore need to be navigated to by object or marked KeepAlive, or the next visit throws because WPF tries to call a constructor that does not exist. The cost of keeping pages alive is memory; the cost of not keeping them is recreation time and lost state. JournalEntry.Name (attached property) or the page’s Title is the text of the entry in the back/forward drop-down; RemoveBackEntry removes the last back entry, useful after a "wizard" step you do not want the user to return to.

JournalOwnership on a Frame decides whose journal it uses:

Value Behavior

Automatic (default)

Uses the parent’s journal if it is hosted in a NavigationWindow or another Frame; otherwise owns one.

OwnsJournal

The frame keeps its own journal. Add NavigationUIVisibility="Hidden" if you do not want the frame’s own back/forward buttons.

UsesParentJournal

Navigations in the frame are recorded in the parent’s journal; throws when there is no parent journal.

Navigation events

NavigationService raises Navigating (cancellable), Navigated, NavigationProgress, NavigationFailed, NavigationStopped, LoadCompleted and FragmentNavigation. The same events are re-raised on Application. A page that needs to subscribe must unsubscribe in Unloaded, or a long-lived NavigationService keeps it alive.

A hosted settings area: Frame and a page-first navigation service

Bookshelf Desktop has a settings area made of pages hosted in a Frame inside MainWindow.

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <StackPanel Margin="8">
        <Button Content="General" Command="{Binding OpenGeneralCommand}"/>
        <Button Content="Appearance" Command="{Binding OpenAppearanceCommand}"/>
        <Button Content="Back" Command="{Binding GoBackCommand}"/>
    </StackPanel>

    <Frame x:Name="SettingsFrame" Grid.Column="1"
           NavigationUIVisibility="Hidden"
           JournalOwnership="OwnsJournal"/>
</Grid>
using System.Windows.Controls;
using Microsoft.Extensions.DependencyInjection;

namespace Bookshelf.Desktop;

// Page-local helper for the settings Frame; not the MVVM INavigationService from Bookshelf.Core.
public interface IFrameNavigator
{
    bool CanGoBack { get; }
    void NavigateTo<TPage>() where TPage : Page;
    void GoBack();
}

public sealed class SettingsFrameNavigator(IServiceProvider services) : IFrameNavigator
{
    private Frame? _frame;

    /// <summary>Called once by the view that owns the Frame, from its Loaded handler.</summary>
    public void Attach(Frame frame) => _frame = frame;

    public bool CanGoBack => _frame?.CanGoBack ?? false;

    public void NavigateTo<TPage>() where TPage : Page
    {
        // Navigating to an object keeps that instance in the journal.
        _frame?.Navigate(services.GetRequiredService<TPage>());
    }

    public void GoBack()
    {
        if (_frame is { CanGoBack: true })
        {
            _frame.GoBack();
        }
    }
}
using System.Windows;
using System.Windows.Controls;

namespace Bookshelf.Desktop;

public partial class MainWindow
{
    // Injected through the constructor in the host-created window (not shown).
    private SettingsFrameNavigator _settingsNavigator = null!;

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        // The Frame exists once the markup is loaded; hand it to the service.
        _settingsNavigator.Attach(SettingsFrame);
    }
}

IFrameNavigator is a page-local, Frame/page-first helper, NavigateTo<TPage>() where TPage : Page, used only by the settings Frame. It is not the ViewModel-first INavigationService in Bookshelf.Core that Bookshelf Desktop uses from the Navigation, Dialogs & Services in MVVM page onwards (NavigateTo<TViewModel>()), although an implementation of that service can sit on top of a Frame like this one. It is again a user pattern: Learn documents Frame and NavigationService, not a ViewModel-first navigation abstraction. The Attach call is the usual compromise: the service needs the Frame, which only exists once the view is loaded.

Passing results back: PageFunction

Navigation has no call/return semantics: nothing guarantees that the page you left will be returned to. A PageFunction<T> adds them. It is a Page with a typed result: the caller navigates to it, subscribes to its Returned event, and the page calls OnReturn with a ReturnEventArgs<T> when it finishes. By default a finished PageFunction is removed from the journal (RemoveFromJournal), so Back does not return to a completed step. It is a good fit for a wizard such as Add book by ISBN:

<PageFunction x:Class="Bookshelf.Desktop.IsbnLookupPage"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:sys="clr-namespace:System;assembly=System.Runtime"
              x:TypeArguments="sys:String"
              Title="Look up ISBN">
    <StackPanel Margin="16">
        <TextBox x:Name="IsbnBox"/>
        <Button Content="Accept" IsDefault="True" Click="OnAccept"/>
        <Button Content="Cancel" IsCancel="True" Click="OnCancel"/>
    </StackPanel>
</PageFunction>
using System.Windows;
using System.Windows.Navigation;

namespace Bookshelf.Desktop;

public partial class IsbnLookupPage : PageFunction<string>
{
    public IsbnLookupPage() => InitializeComponent();

    private void OnAccept(object sender, RoutedEventArgs e) =>
        OnReturn(new ReturnEventArgs<string>(IsbnBox.Text.Trim()));

    // Returning with a null Result tells the caller the user cancelled.
    private void OnCancel(object sender, RoutedEventArgs e) =>
        OnReturn(new ReturnEventArgs<string>());
}

public static class AddBookFlow
{
    public static void Start(NavigationService navigation, Action<string> onIsbn)
    {
        var page = new IsbnLookupPage();
        page.Returned += (_, e) =>
        {
            if (e.Result is { Length: > 0 } isbn)
            {
                onIsbn(isbn);
            }
        };
        navigation.Navigate(page);
    }
}

Pages or a view-model-driven shell?

Many MVVM apps do not use Page/Frame at all. They bind a ContentControl to a CurrentViewModel property and let an implicit DataTemplate (matched by DataType) pick the view; that approach, and the INavigationService that drives it, is described on the Navigation, Dialogs & Services in MVVM page.

WPF, WinUI 3 and Windows Forms compared

Concept WPF WinUI 3 (Windows App SDK 2.x) Windows Forms

Window type

System.Windows.Window

Microsoft.UI.Xaml.Window (not a DependencyObject; no Owner, no DataContext), backed by AppWindow

System.Windows.Forms.Form

Modal window

ShowDialog() returns bool?

No modal window; use ContentDialog (inside the XAML tree) with ShowAsync()

ShowDialog() returns DialogResult; ShowDialogAsync is stable in .NET 10

Message box

MessageBox.Show

ContentDialog; MessageDialog needs InitializeWithWindow in desktop apps

MessageBox.Show

File and folder pickers

OpenFileDialog, SaveFileDialog, OpenFolderDialog

FileOpenPicker, FolderPicker (initialize with the window handle)

OpenFileDialog, SaveFileDialog, FolderBrowserDialog

Page navigation

Frame / Page / NavigationService

Frame / Page, plus NavigationView; the frame exposes BackStack and GoBack

None built in; swap UserControls in a panel

The WinUI 3 and Windows Forms cells are written from general platform knowledge, not from the WPF sources of this page; verify them against the WinUI and Windows Forms documentation before relying on them.

Pitfalls

  • Forgetting Owner. A dialog without an owner can open behind the main window and be missed. Set it, or use a dialog service that does.

  • Reopening a closed window. Show on a closed window throws InvalidOperationException. Create a new instance.

  • Setting DialogResult after Show. It is only legal for windows opened with ShowDialog.

  • Blocking the UI thread. ShowDialog and MessageBox.Show pump messages: timers and commands keep running and can re-enter the code that opened the dialog. Disable the command while a dialog is open (an [RelayCommand] with AllowConcurrentExecutions = false does this for async commands).

  • Doing async work in Closing. The event is synchronous; see the discussion above.

  • Shutting down when the last dialog closes. ShutdownMode defaults to OnLastWindowClose: if your startup window is closed before another window is shown, the app exits. Use OnExplicitShutdown when a login window precedes the main window.

  • Pages without a parameterless constructor. The default journal recreates pages by metadata. Navigate by object or set KeepAlive.

  • Memory leaks through the journal. Kept-alive pages and event subscriptions to NavigationService live as long as the journal. Unsubscribe in Unloaded and call RemoveBackEntry when appropriate.

  • Navigating to untrusted URIs. Frame.Source with a user-supplied value can load XAML or HTML; navigate only to known pages (see Loading XAML at runtime on the XAML Language Fundamentals page).

  • Window handles on the wrong thread. Windows have thread affinity; create and show them on the UI thread only.

What changed since .NET Framework

  • The APIs described here are the same as in .NET Framework 4.8, except for the items below. Only the Learn pages that introduce OpenFolderDialog (.NET 8) and the .NET 10 MessageBox enums carry version information.

  • OpenFolderDialog is new in .NET 8; before that WPF apps used System.Windows.Forms.FolderBrowserDialog or the Windows API Code Pack.

  • .NET 10 adds AbortRetryIgnore, RetryCancel and CancelTryContinue buttons and the matching results.

  • XAML Browser Applications (XBAP) and their partial-trust sandbox are gone; the navigation model remains for standalone apps and the old Code Access Security remnants were removed in .NET 10. Navigation examples that mention Internet Explorer or the browser’s back button do not apply.

  • .NET 10 adds the Fluent style for NavigationWindow among other controls; see Themes & Fluent Design.

What Microsoft Learn does not provide

  • There is no official dialog service or ViewModel-first navigation API for WPF, and CommunityToolkit.Mvvm has none either (it also has no ViewModelLocator). IDialogService, INavigationService and IFrameNavigator here are user patterns.

  • The navigation overview was written for XBAP and IE-hosted apps; much of it (cookies, the browser journal UI, partial trust) is historical. There is no modern guidance on Frame and Page in an MVVM application.

  • The Learn pages for MessageBox do not document the .NET 10 additions beyond the what’s-new article; the enum values in the tables above come from it.

  • Accessibility of custom dialogs (focus return, announcing the dialog) is not covered; see Accessibility & UI Automation.