Navigation, Dialogs & Services in MVVM
|
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. |
A view model must be able to say "show the book editor", "ask the user to confirm" or "go back" without knowing what a
Window, a MessageBox or a Frame is. If it does know, it can no longer be unit tested, and it cannot be reused on
another UI stack. The usual answer is a small service interface that the view model depends on, with the UI-specific
implementation registered in the dependency injection container. This page shows the services that Bookshelf Desktop
needs: INavigationService, IDialogService and IFileDialogService. 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 (11.0.0-rc.2) and general availability is
expected at .NET Conf, 10 to 13 November 2026; none of the APIs used here is listed as changed for it.
These services are user patterns. There is no official navigation or dialog API for MVVM in WPF, WinUI 3 or
CommunityToolkit.Mvvm. Microsoft documents the building blocks (Window.ShowDialog, MessageBox, Frame,
NavigationService, ContentDialog, NavigationView) and, in the .NET MAUI enterprise book, the idea of a
view-model-first navigation service. The interfaces below are therefore a design, not an API you can look up, and the
toolkit has no ViewModelLocator or DialogService either (those belonged to MvvmLight). The Windows, Dialogs & Navigation page introduced a
synchronous, older form of IDialogService and a Frame-first INavigationService; the ViewModel-first, asynchronous
form on this page refines it, and Bookshelf Desktop uses these shapes from here on. The MVVM Pattern and MVVM Toolkit
pages explain the pattern and the library; Dependency Injection & the Generic Host shows the registration; Messaging Between View Models covers IMessenger in depth; Testing View Models & UI uses the fakes shown here.
Why services, and what Microsoft says
The MAUI enterprise book lists the problems any MVVM navigation design has to solve. They apply unchanged to WPF and WinUI 3.
| Problem named in the book | How this page answers it |
|---|---|
Identify the target view without coupling views to each other |
The view model names the target by view-model type, |
Instantiate and initialize view and view model together (with a DI container) |
The service resolves the view model from the container; a |
View-first or view-model-first navigation |
This page is view-model-first. The view-first Frame form is the older variant in Windows, Dialogs & Navigation. |
Navigation initiated from the view model, not only from the view |
The service is injected into view models; commands call it. |
Pass parameters to the destination |
|
Confirm or cancel navigation (unsaved changes) |
The view model awaits |
The book’s own INavigationService is route-based (NavigateToAsync(string route, IDictionary<string, object>),
PopAsync, InitializeAsync) and is tied to .NET MAUI Shell; it says explicitly that it is simplistic and that other
navigation mechanisms need their own implementation. It is a design reference here, not code to copy.
The Bookshelf Desktop services at a glance
| Service | Contract (Bookshelf.Core, except IFileDialogService in Bookshelf.Desktop) |
Implemented by |
|---|---|---|
|
|
|
|
|
|
|
|
|
All three are asynchronous or return plain values, never UI types. Each returns the user’s decision (a bool, a Book?,
a path string), so a fake in a test can answer instantly.
ViewModel-first navigation with a ContentControl
The most common WPF form is a single shell window whose content area is a ContentControl bound to the current view
model. WPF picks the view with an implicit DataTemplate: a template with a DataType and no x:Key is applied
automatically to any object of that type shown by a content presenter. This is documented WPF behaviour (see the
data-templating overview); the navigation service on top of it is the user pattern.
The contract and an optional parameter hook
namespace Bookshelf.Core;
public interface INavigationService
{
bool CanGoBack { get; }
/// <summary>Shows the view for <typeparamref name="TViewModel"/>.</summary>
void NavigateTo<TViewModel>() where TViewModel : class;
/// <summary>Shows the view and hands <paramref name="parameter"/> to the new view model.</summary>
void NavigateTo<TViewModel>(object parameter) where TViewModel : class;
void GoBack();
}
/// <summary>Optional: implemented by view models that accept a navigation parameter.</summary>
public interface INavigationAware
{
void OnNavigatedTo(object? parameter);
}
The service
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
namespace Bookshelf.Desktop;
public sealed partial class ViewModelNavigationService(IServiceProvider services)
: ObservableObject, INavigationService
{
private readonly Stack<object> _backStack = new();
/// <summary>The view model shown by the shell; bound by the ContentControl.</summary>
[ObservableProperty]
public partial object? Current { get; set; }
public bool CanGoBack => _backStack.Count > 0;
public void NavigateTo<TViewModel>() where TViewModel : class => Show<TViewModel>(null);
public void NavigateTo<TViewModel>(object parameter) where TViewModel : class => Show<TViewModel>(parameter);
public void GoBack()
{
if (_backStack.Count == 0)
{
return;
}
// The previous instance is restored, so scroll position, selection and edits survive.
Current = _backStack.Pop();
OnPropertyChanged(nameof(CanGoBack));
}
private void Show<TViewModel>(object? parameter) where TViewModel : class
{
var next = services.GetRequiredService<TViewModel>();
if (next is INavigationAware aware)
{
aware.OnNavigatedTo(parameter);
}
if (Current is not null)
{
_backStack.Push(Current);
}
Current = next;
OnPropertyChanged(nameof(CanGoBack));
}
}
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Bookshelf.Desktop.ViewModels;
public sealed partial class ShellViewModel : ObservableObject
{
private readonly INavigationService _navigation;
public ShellViewModel(ViewModelNavigationService navigation)
{
Navigation = navigation;
_navigation = navigation;
navigation.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(INavigationService.CanGoBack))
{
GoBackCommand.NotifyCanExecuteChanged();
}
};
_navigation.NavigateTo<BooksViewModel>();
}
/// <summary>Exposed so XAML can bind Navigation.Current.</summary>
public ViewModelNavigationService Navigation { get; }
private bool CanGoBack => _navigation.CanGoBack;
[RelayCommand(CanExecute = nameof(CanGoBack))]
private void GoBack() => _navigation.GoBack();
}
Source: RelayCommand attribute
<Window x:Class="Bookshelf.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Bookshelf.Desktop.ViewModels"
xmlns:views="clr-namespace:Bookshelf.Desktop.Views"
Title="Bookshelf Desktop" Width="900" Height="600">
<Window.Resources>
<!-- No x:Key: these are implicit templates, matched by DataType. -->
<DataTemplate DataType="{x:Type vm:BooksViewModel}">
<views:BooksView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:BookEditorViewModel}">
<views:BookEditorView />
</DataTemplate>
</Window.Resources>
<DockPanel>
<ToolBar DockPanel.Dock="Top">
<Button Content="Back" Command="{Binding GoBackCommand}" />
</ToolBar>
<ContentControl Content="{Binding Navigation.Current}" />
</DockPanel>
</Window>
Source: Data templating overview
Registration in the Generic Host (the host itself is covered in Dependency Injection & the Generic Host):
using Bookshelf.Core;
using Bookshelf.Desktop.ViewModels;
using Microsoft.Extensions.DependencyInjection;
namespace Bookshelf.Desktop;
public static class NavigationRegistration
{
public static IServiceCollection AddBookshelfNavigation(this IServiceCollection services)
{
// One instance, visible both as the concrete type (for the shell) and as the interface.
services.AddSingleton<ViewModelNavigationService>();
services.AddSingleton<INavigationService>(sp => sp.GetRequiredService<ViewModelNavigationService>());
// View models are transient: every NavigateTo gets a fresh instance.
services.AddTransient<BooksViewModel>();
services.AddTransient<BookEditorViewModel>();
services.AddSingleton<ShellViewModel>();
return services;
}
}
Source: Dependency injection overview
This is a user pattern; WPF supplies only the implicit DataTemplate resolution and the data binding. The
BookEditorViewModel is built from a Book, so it cannot be created by the container alone: in practice it implements
INavigationAware and is told the Book by OnNavigatedTo, or the application uses the dialog route shown below instead
of navigation for editing. Treat the registration above as a sketch of the shape, not as a finished editor flow.
A Frame-based variant
When the shell must use Frame (for the WPF journal, page transitions, or because the pages already exist), the same
ViewModel-first contract can sit on top of it. The service keeps a map from view-model type to page type, creates the
page, assigns its DataContext, and lets the Frame own the back stack. The Frame-first variant,
NavigateTo<TPage>() where TPage : Page, is in Windows, Dialogs & Navigation.
using System.Windows.Controls;
using Bookshelf.Core;
using Microsoft.Extensions.DependencyInjection;
namespace Bookshelf.Desktop;
public sealed class FrameViewModelNavigationService(
IServiceProvider services,
IReadOnlyDictionary<Type, Type> pageForViewModel) : INavigationService
{
private Frame? _frame;
/// <summary>Called once by the shell, from its Loaded handler.</summary>
public void Attach(Frame frame) => _frame = frame;
public bool CanGoBack => _frame?.CanGoBack ?? false;
public void NavigateTo<TViewModel>() where TViewModel : class => Show(typeof(TViewModel), null);
public void NavigateTo<TViewModel>(object parameter) where TViewModel : class => Show(typeof(TViewModel), parameter);
public void GoBack()
{
if (_frame is { CanGoBack: true })
{
_frame.GoBack();
}
}
private void Show(Type viewModelType, object? parameter)
{
var viewModel = services.GetRequiredService(viewModelType);
if (viewModel is INavigationAware aware)
{
aware.OnNavigatedTo(parameter);
}
var page = (Page)services.GetRequiredService(pageForViewModel[viewModelType]);
page.DataContext = viewModel;
// Navigating to an object keeps that instance (and its DataContext) in the journal.
_frame?.Navigate(page);
}
}
Source: Navigation overview
ContentControl + implicit templates |
Frame + pages |
|
|---|---|---|
Back stack |
Your own ( |
The WPF journal, shared with |
Page lifetime |
The view is recreated by the template; the view model instance is what you keep |
Pages are kept alive when you navigate to an instance; see |
Transitions, browser-style chrome |
None built in |
|
Cancelling navigation |
Your service decides before it changes |
Handle |
Typical use |
Line-of-business shells, master/detail workspaces |
Wizards, settings areas, apps that need the journal |
IDialogService with a WPF implementation
Dialogs are modal questions. Making them asynchronous (Task<bool>, Task<Book?>) lets the same interface work on
WPF, where ShowDialog blocks the calling code, and on WinUI 3, where ContentDialog.ShowAsync genuinely awaits. In WPF
the returned task is usually already completed; the shape is chosen for the more demanding stack.
namespace Bookshelf.Core;
public interface IDialogService
{
/// <summary>Asks a yes/no question; true means the user confirmed.</summary>
Task<bool> ConfirmAsync(string title, string message);
/// <summary>Opens the book editor; returns the edited Book, or null when the user cancelled.</summary>
Task<Book?> EditBookAsync(Book? existing);
}
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<bool> ConfirmAsync(string title, string message)
{
var answer = MessageBox.Show(
GetOwner(), message, title,
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
return Task.FromResult(answer == MessageBoxResult.Yes);
}
public Task<Book?> EditBookAsync(Book? existing)
{
var editor = new BookEditorViewModel(existing ?? new Book(0, string.Empty, string.Empty));
var window = ActivatorUtilities.CreateInstance<BookEditorWindow>(services, editor);
window.Owner = GetOwner();
// ShowDialog blocks here until the window closes.
var saved = window.ShowDialog() == true;
return Task.FromResult(saved ? editor.ToBook() : null);
}
// The active window is the natural owner; fall back to the main window.
private static Window? GetOwner() =>
Application.Current.Windows.OfType<Window>().FirstOrDefault(w => w.IsActive)
?? Application.Current.MainWindow;
}
Source: Dialog boxes overview
using System.Windows;
using Bookshelf.Desktop.ViewModels;
namespace Bookshelf.Desktop;
public partial class BookEditorWindow : Window
{
private readonly BookEditorViewModel _editor;
public BookEditorWindow(BookEditorViewModel editor)
{
InitializeComponent();
_editor = editor;
DataContext = editor;
}
// The Save button has IsDefault="True"; Cancel has IsCancel="True" and closes the window by itself.
private void OnSave(object sender, RoutedEventArgs e)
{
// Validate() is the editor's own method, shown below.
if (_editor.Validate())
{
DialogResult = true;
}
}
}
Source: Dialog boxes overview
The editor’s Validate() method, a member of the canonical BookEditorViewModel(Book book) shown on the
MVVM Toolkit (CommunityToolkit.Mvvm) page:
// Member of Bookshelf.Desktop.ViewModels.BookEditorViewModel : ObservableValidator.
public bool Validate()
{
ValidateAllProperties();
return !HasErrors;
}
Source: ObservableValidator
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Bookshelf.Desktop.ViewModels;
// Members of BooksViewModel, added inside the class declaration that owns the primary constructor.
[RelayCommand]
private async Task EditSelectedAsync()
{
if (SelectedBook is null)
{
return;
}
// The view model never sees a Window: it sends a Book and gets a Book back.
var edited = await dialogs.EditBookAsync(SelectedBook.Model);
if (edited is null)
{
return; // cancelled
}
await repository.UpdateAsync(edited, CancellationToken.None);
await LoadAsync(CancellationToken.None);
}
[RelayCommand]
private async Task DeleteSelectedAsync()
{
if (SelectedBook is not { } selected)
{
return;
}
if (!await dialogs.ConfirmAsync("Delete book", $"Delete \"{selected.Title}\"?"))
{
return;
}
await repository.DeleteAsync(selected.Id, CancellationToken.None);
Books.Remove(selected);
}
Source: Dialog boxes overview
BooksViewModel is the canonical BooksViewModel(IBookRepository repository, IDialogService dialogs); the two commands
above belong inside that class (primary-constructor parameters are not visible from another partial part), and LoadAsync is its existing load method. BookEditorViewModel is the
ObservableValidator described on the MVVM Toolkit (CommunityToolkit.Mvvm) page (validation
itself is covered on the Validation page), built from a Book and producing one with
ToBook(). Validate() is a method of this application’s own editor view model; it is not a toolkit member.
Unit-testing against the services
Because the contracts hold no UI types, a hand-written fake is enough, and no mocking library is required.
using Bookshelf.Core;
namespace Bookshelf.Desktop.Tests;
public sealed class FakeDialogService : IDialogService
{
public bool ConfirmAnswer { get; set; } = true;
public Book? EditResult { get; set; }
public List<string> Questions { get; } = [];
public Task<bool> ConfirmAsync(string title, string message)
{
Questions.Add(message);
return Task.FromResult(ConfirmAnswer);
}
public Task<Book?> EditBookAsync(Book? existing) => Task.FromResult(EditResult);
}
IFileDialogService
File and folder pickers are the same problem: a UI object that blocks and needs an owner window. The contract returns paths only.
namespace Bookshelf.Desktop;
public interface IFileDialogService
{
/// <summary>Asks for a CSV file to import; null when cancelled.</summary>
Task<string?> PickFileToImportAsync();
/// <summary>Asks where to export the catalogue; null when cancelled.</summary>
Task<string?> PickFileToSaveAsync(string suggestedName);
/// <summary>Asks for a backup folder; null when cancelled.</summary>
Task<string?> PickFolderAsync();
}
using System.Windows;
using Microsoft.Win32;
namespace Bookshelf.Desktop;
public sealed class WpfFileDialogService : IFileDialogService
{
public Task<string?> PickFileToImportAsync()
{
var dialog = new OpenFileDialog
{
Title = "Import books",
Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*",
CheckFileExists = true,
};
return Task.FromResult(dialog.ShowDialog(Owner()) == true ? dialog.FileName : null);
}
public Task<string?> PickFileToSaveAsync(string suggestedName)
{
var dialog = new SaveFileDialog
{
Title = "Export books",
FileName = suggestedName,
DefaultExt = ".csv",
Filter = "CSV files (*.csv)|*.csv",
OverwritePrompt = true,
};
return Task.FromResult(dialog.ShowDialog(Owner()) == true ? dialog.FileName : null);
}
public Task<string?> PickFolderAsync()
{
var dialog = new OpenFolderDialog { Title = "Backup folder" };
return Task.FromResult(dialog.ShowDialog(Owner()) == true ? dialog.FolderName : null);
}
private static Window? Owner() => Application.Current.MainWindow;
}
Source: How to open a common dialog box
OpenFolderDialog was added to WPF in .NET 8; on .NET Framework only OpenFileDialog and SaveFileDialog exist. The
view model passes the returned path to ordinary file code; if it then reads the file, it does so off the UI thread, as
described in Threading, Dispatcher & Async.
IMessenger or a service?
IMessenger (CommunityToolkit.Mvvm) is also used to make a view model request something from the UI, so the two tools are
often confused. They answer different questions.
Service (IDialogService, INavigationService) |
IMessenger |
|
|---|---|---|
Shape |
Request/response: the caller waits for an answer |
Publish/subscribe: the sender does not know who listens, or whether anyone does |
Result |
A typed return value ( |
None, unless you use a request message such as |
Coupling |
The consumer depends on one interface; easy to find and to fake |
The sender and receiver share a message type; the wiring is implicit |
Failure when nobody is there |
A missing registration fails at startup or at first resolve |
The message is silently dropped |
Best for |
"Ask the user", "go to", "pick a file" |
"A book was saved, refresh the list", broadcasting state to unrelated view models |
Source: Messenger
A good split for Bookshelf Desktop: EditBookAsync returns the edited book to the caller through the service, and the
caller then sends a BookSavedMessage through the messenger so the other open view models can refresh. Using a message to
ask a question (and waiting for a reply) is possible with request messages but turns a typed call into an indirect one;
prefer a service for that. See Messaging Between View Models for the details.
The same ideas on WinUI 3
WinUI 3 has the same two halves: a shell that hosts content, and dialog/picker objects that need a UI context.
NavigationView plus Frame
NavigationView is a navigation chrome; it does not navigate by itself. The Learn page says that it only shows the item
as selected and raises ItemInvoked (and SelectionChanged), and that you handle those events to navigate and update the
header. It has a built-in back button that does not navigate on its own either: you handle BackRequested, and you
control the button with IsBackButtonVisible and IsBackEnabled (which can be bound to Frame.CanGoBack).
PaneDisplayMode defaults to Auto, which adapts between LeftMinimal, LeftCompact and Left as the window width
changes.
<NavigationView x:Name="NavView"
PaneDisplayMode="Auto"
IsBackButtonVisible="Visible"
IsBackEnabled="{x:Bind ContentFrame.CanGoBack, Mode=OneWay}"
SelectionChanged="OnSelectionChanged"
BackRequested="OnBackRequested">
<NavigationView.MenuItems>
<NavigationViewItem Content="Books" Tag="Bookshelf.WinUI.Views.BooksPage" />
<NavigationViewItem Content="Statistics" Tag="Bookshelf.WinUI.Views.StatisticsPage" />
</NavigationView.MenuItems>
<Frame x:Name="ContentFrame" />
</NavigationView>
using Bookshelf.WinUI.Views;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
namespace Bookshelf.WinUI;
public sealed partial class ShellPage : Page
{
public ShellPage() => InitializeComponent();
private void OnSelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
if (args.IsSettingsSelected)
{
ContentFrame.Navigate(typeof(SettingsPage), null, new EntranceNavigationTransitionInfo());
}
else if (args.SelectedItemContainer?.Tag is string pageTypeName
&& Type.GetType(pageTypeName) is { } pageType)
{
ContentFrame.Navigate(pageType, null, new EntranceNavigationTransitionInfo());
}
}
private void OnBackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args)
{
if (ContentFrame.CanGoBack)
{
ContentFrame.GoBack();
}
}
}
Source: NavigationView
That is page-first navigation in code-behind, which is the documented sample. To make it ViewModel-first, give the shell
one INavigationService implementation that wraps the same Frame, exactly like FrameViewModelNavigationService
above, but calling Frame.Navigate(Type, object?) (WinUI’s Frame navigates to a page type, not to a page instance,
so the page is created by the frame and the service sets its view model in Navigated, or the page resolves it from the
container). The NavigationView item selection is then translated into NavigateTo<TViewModel>() calls by the shell
view model, or left in the code-behind as the single place that touches page types.
ContentDialog as IDialogService
WinUI 3 has no MessageBox and no DialogResult; the dialog control is ContentDialog, awaited with ShowAsync(),
which returns a ContentDialogResult (Primary, Secondary, or None for the close button). Two rules matter to a
service: you must set XamlRoot, otherwise it throws at runtime, and only one ContentDialog can be open per
window, so a second request throws until the first closes.
using Bookshelf.Core;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Bookshelf.WinUI;
public sealed class WinUiDialogService : IDialogService
{
private XamlRoot? _xamlRoot;
/// <summary>Called by the shell page when it is loaded.</summary>
public void Attach(XamlRoot xamlRoot) => _xamlRoot = xamlRoot;
public async Task<bool> ConfirmAsync(string title, string message)
{
var dialog = new ContentDialog
{
XamlRoot = _xamlRoot,
Title = title,
Content = message,
PrimaryButtonText = "Yes",
CloseButtonText = "No",
DefaultButton = ContentDialogButton.Close,
};
return await dialog.ShowAsync() == ContentDialogResult.Primary;
}
public Task<Book?> EditBookAsync(Book? existing)
{
// Same contract: show a ContentDialog subclass that hosts the editor and return Book or null.
throw new NotImplementedException("Bookshelf editor ContentDialog is not shown on this page.");
}
}
Source: Dialog controls (ContentDialog)
WPF, WinUI 3 and Windows Forms compared
| WPF | WinUI 3 | Windows Forms | |
|---|---|---|---|
Shell content host |
|
|
A |
Pick view by view model |
Implicit |
|
A dictionary of view model type to control type in your service |
Back stack |
Own stack, or the |
|
Own stack |
Modal dialog |
|
|
|
File pickers |
|
Windows App SDK pickers; desktop apps must associate them with a window handle |
|
Official MVVM navigation API |
None |
None ( |
None |
Choosing a form
| Situation | Recommendation |
|---|---|
One shell, several top-level workspaces, line-of-business app |
|
Wizard or settings area, or you need the WPF journal |
|
WinUI 3 app with a side menu |
|
Edit form that returns a value |
|
Notify unrelated view models that something changed |
|
Pitfalls
-
The container cannot create everything. A view model that needs a
Bookin its constructor cannot be resolved byGetRequiredService. Use an initialization hook such asINavigationAware, a factory, orActivatorUtilities. -
Singleton versus transient view models. A transient view model gives a fresh screen on every
NavigateTo; a singleton keeps its state but can hold stale data. Choose deliberately per view model. -
The back stack holds references. Every view model on the stack stays alive, with its event subscriptions. Clear the stack when the user leaves a workflow, and unsubscribe or use
WeakReferenceMessengerfor message handlers. -
ShowDialogpumps messages. Code afterShowDialogruns only when the dialog closes, but other windows continue to process messages meanwhile; guard against a command running twice by disabling it while the dialog is open. -
Owner windows. A dialog without an
Ownercan appear behind the main window or in the task switcher. Use the active window, as inGetOwner(). -
UI thread only. Dialog services touch UI objects; call them from the UI thread. A
RelayCommandruns there, but code after aConfigureAwait(false)does not. -
One
ContentDialogper window on WinUI 3. Queue requests in the service, or a secondConfirmAsyncthrows. -
Unsaved changes. Navigation guards are a convention: have the view model ask
ConfirmAsyncbefore it callsNavigateToorGoBack; the service does not know about unsaved data. -
Hidden coupling through code-behind. A view that calls
NavigationServiceorMessageBoxdirectly defeats the pattern and the tests. -
MessageBoxis not themed. The WPFMessageBoxuses the system look; with Fluent theming, use your own dialog window if consistency matters.
What changed across .NET versions
| Version | Relevant change |
|---|---|
.NET Framework 4.x |
|
.NET Core 3.x to .NET 7 |
Same WPF APIs; MVVM services are always user code. WinUI 3 arrives with Windows App SDK. |
.NET 8 |
|
.NET 9 and 10 |
WPF |
.NET 11 (RC2) |
No change to these APIs is listed. |
What Microsoft Learn does not provide
-
No official
INavigationService,IDialogServiceorIFileDialogServicefor WPF, WinUI 3, Windows Forms or the toolkit. The only official design reference is the .NET MAUI book, whose service is tied to MAUIShell. -
No WPF-specific MVVM navigation article; the WPF navigation overview documents
Frame,Page,NavigationServiceand the journal as browser-style navigation. -
No guidance for testing navigation or dialog services; the unit-testing chapter of the MAUI book is general.
-
No documentation of a view-model-to-view mapping convention for WinUI 3.
References
-
Navigation (Enterprise Application Patterns Using .NET MAUI)
-
Unit testing (Enterprise Application Patterns Using .NET MAUI)
-
Windows, Dialogs & Navigation for
Window,MessageBox,Frameand the journal -
Commands for the commands that call these services
-
Validation for the editor view model
-
Threading, Dispatcher & Async for the UI-thread rules