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 Owner before ShowDialog, or the dialog can disappear behind the main window.
  • DialogResult only works on a window shown with ShowDialog; 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 approachKeeps old views aliveGives back navigationUse when
ContentControl plus templatesNoWrite it yourselfA view model drives the flow
TabControlYes, all tabsNoA small fixed set of panels
Frame with PageOnly in the journalYesWizard flows and back buttons
Separate windowsYesNoGenuinely 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.

Validation, converters and input handling Asynchronous work and UI responsiveness

Last refreshed 2026-09-18.