Navigation, windows and dialogs
Modal windows with ownership and results, a navigation service that a view model can use without referencing WPF, frame and tab navigation, and window lifetime rules.
Windows and modal dialogs
// the dialog stays in front, and we read the result afterwards
var dialog = new CustomerEditWindow { Owner = this, DataContext = new CustomerEditViewModel(id) };
if (dialog.ShowDialog() == true)
await _orders.ReloadAsync();
// and inside the dialog
private void OnSave(object sender, RoutedEventArgs e)
{
if (!_viewModel.Validate()) return;
DialogResult = true; // also closes the window
}- Set
OwnerbeforeShowDialog, or the dialog can disappear behind the main window. DialogResultonly works on a window shown withShowDialog; setting it on a modeless window throws.- A view model should not open windows directly. Inject a navigation or dialog service so the view model stays testable.
- Closing the main window ends the application by default (
ShutdownMode). Change it only if you add a tray icon or a splash window.
A navigation service
public interface IDialogService
{
bool? ShowCustomerEditor(Guid customerId);
Task<bool> ConfirmAsync(string title, string message);
}
public sealed class DialogService : IDialogService
{
public bool? ShowCustomerEditor(Guid customerId)
{
var vm = new CustomerEditViewModel(customerId);
var window = new CustomerEditWindow { DataContext = vm, Owner = Application.Current.MainWindow };
return window.ShowDialog();
}
public Task<bool> ConfirmAsync(string title, string message)
{
var result = MessageBox.Show(Application.Current.MainWindow, message, title,
MessageBoxButton.OKCancel, MessageBoxImage.Question) == MessageBoxResult.OK;
return Task.FromResult(result);
}
}// the view model depends on the abstraction, so it can be tested with a fake
public sealed class OrdersViewModel
{
private readonly IDialogService _dialogs;
public OrdersViewModel(IDialogService dialogs) => _dialogs = dialogs;
public async Task EditAsync()
{
if (Selected is null) return;
if (_dialogs.ShowCustomerEditor(Selected.Id) == true)
await ReloadAsync();
}
}For a multi-view application, either set a single ContentControl whose content is the current view model, or use a Frame with pages. The ContentControl plus data templates keeps the view model in charge; the Frame gives you a journal and back navigation for free.
Tabs, frames and lifetime
<!-- one content host, a template that picks the view for the current view model -->
<Window.Resources>
<DataTemplate DataType="{x:Type vm:OrdersViewModel}">
<views:OrdersView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsViewModel}">
<views:SettingsView />
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding Current}" />
<!-- a TabControl keeps every tab alive; that is a memory decision, not just a layout one -->
<TabControl ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}" />| Navigation approach | Keeps old views alive | Gives back navigation | Use when |
|---|---|---|---|
ContentControl plus templates | No | Write it yourself | A view model drives the flow |
TabControl | Yes, all tabs | No | A small fixed set of panels |
Frame with Page | Only in the journal | Yes | Wizard flows and back buttons |
| Separate windows | Yes | No | Genuinely independent documents |
💡
Unsubscribe from events and dispose timers in
OnClosed, not in a finaliser. A view model subscribed to a long-lived service is the most common WPF memory leak, and it looks like the application slowing down over a working day rather than a crash.FAQ
How do I pass a result back from a dialog?
Prefer
ShowDialog() == true plus a result property on the dialog's view model. Making the parent subscribe to a child event creates a reference in the wrong direction and leaks the parent.Modal or modeless for a progress window?
Modeless if the user can keep working, modal if they cannot. A modal progress dialog that blocks a cancellable operation is the worst of both - offer Cancel and keep the UI thread free.
Related
Validation, converters and input handling Asynchronous work and UI responsiveness
Last refreshed 2026-09-18.