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 |
|---|---|---|
|
string, |
Caption and icon of the non-client area and the taskbar button. |
|
double |
The allowed size range. |
|
|
Fit the window to its content when it is first shown. Min/Max values win over it. |
|
|
Where the window first appears. With |
|
|
Current state. |
|
|
The border and caption buttons. |
|
|
Whether and how the user can resize the window. |
|
bool |
Taskbar button; topmost z-order. |
|
bool |
Set to |
|
|
The ownership relationship described below. |
|
|
Only valid for a window shown with |
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 |
|---|---|
|
The first time the window is shown, once its |
|
The window becomes the foreground window of the app. Raised before |
|
The element tree is built, laid out and ready for rendering. |
|
The content has been rendered; the window can be considered opened. Use it for work that must wait until the first paint. |
|
The user switched to another window or app. |
|
Raised before the window closes. The handler receives |
|
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.
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.
Source: Windows in WPF overview
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, |
When the window closes, with |
Other windows of the app |
Stay interactive |
Blocked until it closes |
Result |
A property on the window, an event, or |
|
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.
Source: Dialog boxes overview
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;
}
}
}
Source: Dialog boxes overview
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;
}
Source: ObservableProperty attribute and Dialog boxes overview
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 |
|---|---|
|
Pass it. Without an owner the box is not tied to your window and may appear behind it. |
|
Plain text. No markup, but |
|
The set of buttons; see the next table. |
|
|
|
Which button has focus. Make the safe choice the default for destructive questions ( |
|
|
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 |
|---|---|---|
|
0 |
OK |
|
1 |
OK, Cancel |
|
2 |
Abort, Retry, Ignore (new in .NET 10) |
|
3 |
Yes, No, Cancel |
|
4 |
Yes, No |
|
5 |
Retry, Cancel (new in .NET 10) |
|
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;
}
}
Source: What’s new in WPF for .NET 10
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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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;
}
Source: How to open a common dialog box
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 |
|---|---|
|
Content that can be navigated to: |
|
A |
|
A content control that hosts a page inside any other content, with its own |
|
Does the work: |
|
A page that behaves like a function: it is called by navigation and returns a typed result. |
|
|
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 isFalse).
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 |
|---|---|
|
Uses the parent’s journal if it is hosted in a |
|
The frame keeps its own journal. Add |
|
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);
}
}
Source: Navigation overview
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);
}
}
Source: Structured navigation overview
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 |
|
|
|
Modal window |
|
No modal window; use |
|
Message box |
|
|
|
File and folder pickers |
|
|
|
Page navigation |
|
|
None built in; swap |
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.
Showon a closed window throwsInvalidOperationException. Create a new instance. -
Setting
DialogResultafterShow. It is only legal for windows opened withShowDialog. -
Blocking the UI thread.
ShowDialogandMessageBox.Showpump 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]withAllowConcurrentExecutions = falsedoes this for async commands). -
Doing async work in
Closing. The event is synchronous; see the discussion above. -
Shutting down when the last dialog closes.
ShutdownModedefaults toOnLastWindowClose: if your startup window is closed before another window is shown, the app exits. UseOnExplicitShutdownwhen 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
NavigationServicelive as long as the journal. Unsubscribe inUnloadedand callRemoveBackEntrywhen appropriate. -
Navigating to untrusted URIs.
Frame.Sourcewith 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 10MessageBoxenums carry version information. -
OpenFolderDialogis new in .NET 8; before that WPF apps usedSystem.Windows.Forms.FolderBrowserDialogor the Windows API Code Pack. -
.NET 10 adds
AbortRetryIgnore,RetryCancelandCancelTryContinuebuttons 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
NavigationWindowamong 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,INavigationServiceandIFrameNavigatorhere 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
FrameandPagein an MVVM application. -
The Learn pages for
MessageBoxdo 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.