Guards and Redirects
Routed view models can veto navigations away from them, or redirect navigations to them. This guide covers the guard and redirect APIs and shows common patterns like confirm-on-unsaved-changes and default-child redirects.
Cancelling a Navigation
OnNavigatingAwayAsync(NavigatingArgs) and OnRouteNavigatingAsync(NavigatingArgs) are invoked before any navigation that would unmount the view model. Either can set Cancel to true to veto the navigation:
public override async Task OnNavigatingAwayAsync(NavigatingArgs args)
{
if (!HasUnsavedChanges)
return;
int result = await this.Navigator.ShowMessageDialogAsync(
"You have unsaved changes. Discard them?",
"Unsaved Changes",
DialogButtonLabels.YesNo);
if (result != 0)
args.Cancel = true;
}
- OnNavigatingAwayAsync fires only when this view model is being unmounted (its route part is changing).
- OnRouteNavigatingAsync fires for any navigation involving the current route, including sibling child swaps. Use it on parent view models to guard the whole subtree.
NavigationType indicates whether the navigation is a Normal, Back, Forward or Refresh operation (see NavigationType). Use this to allow back/forward to bypass the guard if appropriate:
if (args.NavigationType == NavigationType.Normal && HasUnsavedChanges)
{
// only guard normal navigations
}
WebAssembly: Browser Tab Close, Refresh, and External Navigation
In-app navigation (including the browser back / forward buttons) goes through the navigator's normal asynchronous pipeline, so OnNavigatingAwayAsync and OnRouteNavigatingAsync can freely await work such as confirmation dialogs.
Closing the tab, refreshing the page, or navigating to an external URL is different. The browser delivers these as a beforeunload event which requires a synchronous decision; the navigator cannot await your guard methods. The navigator still invokes the guards synchronously, but if any guard on a view model in the active route does not complete synchronously, the navigator must block the unload immediately and the browser shows its native prompt:
Leave Site? Changes you made may not be saved.
The wording is browser-controlled and cannot be customized. If the user chooses to leave, the app is terminated immediately - any async work started by the guard is abandoned. If the user chooses to stay, the in-flight guard task continues running to completion in the background, so a subsequent close attempt may complete synchronously without prompting.
Key points:
- The fallback prompt is triggered if any OnNavigatingAwayAsync or OnRouteNavigatingAsync implementation on any active-route view model goes async (i.e. awaits an incomplete task), regardless of whether the view model would have set Cancel.
- A guard that completes synchronously - whether by returning CompletedTask, by being an
asyncmethod that never awaits an incomplete task, or by synchronously setting Cancel totrue- is fully respected and produces no prompt unless Cancel istrue. - The guard is only installed when HookWindowClosedEvents has been called (see WinUI / Uno Setup).
For view models that need to behave well across both paths, cache dirty state synchronously and set Cancel from a synchronous code path. Async confirmation dialogs can still be used; they will be honored on the in-app navigation path and gracefully degrade to the native prompt on tab close / refresh:
public override async Task OnNavigatingAwayAsync(NavigatingArgs args)
{
if (!HasUnsavedChanges)
return;
// On WASM tab-close / refresh, the browser's native "Leave Site?" prompt is
// shown first because this method goes async. If the user chooses to leave,
// the app is terminated immediately; otherwise, execution continues here and
// the dialog below is shown.
int result = await this.Navigator.ShowMessageDialogAsync(
"You have unsaved changes. Discard them?",
"Unsaved Changes",
DialogButtonLabels.YesNo);
if (result != 0)
args.Cancel = true;
}
Redirecting a Navigation
OnNavigatedToAsync(NavigationArgs) and OnRouteNavigatedAsync(NavigationArgs) run after the view model has been materialized but before the user can interact with it. Setting Redirect causes the navigator to immediately perform a different navigation.
public override Task OnNavigatedToAsync(NavigationArgs args)
{
if (!_session.IsAuthenticated)
{
args.Redirect = Redirect.Navigate(Routes.LoginRoot);
}
return Task.CompletedTask;
}
The Redirect class provides static factory methods that mirror the navigator's own APIs:
| Factory | Equivalent |
|---|---|
| Navigate(string) | NavigateAsync (string) |
| Navigate with route parts | NavigateAsync with route parts |
| NavigatePartial | NavigatePartialAsync |
| NavigateToParent<TParentViewModel>(string?) | NavigateToParentAsync |
| GoBack() | GoBackAsync() |
Default Child Redirect Pattern
A parent view model often wants to redirect to a default child when the user navigates to the parent alone:
public override Task OnRouteNavigatedAsync(NavigationArgs args)
{
// Only redirect when the navigation stops at this parent (no child specified).
if (!args.HasChildNavigation)
{
args.Redirect = Redirect.NavigatePartial(Routes.Repo.HomePage);
}
return Task.CompletedTask;
}
HasChildNavigation is true when the navigation target includes a descendant route beneath this view model; skipping the redirect in that case avoids overriding the user's intended destination.
Graceful Shutdown
The guard methods are also consulted by TryShutDownAsync() (see Navigating). The WinUI navigator provides HookWindowClosedEvents which ties everything together:
var navigator = new Navigator(MainContent, ConfigureNavigator);
navigator.HookWindowClosedEvents(this); // 'this' is the Window
When the user closes the window, the navigator intercepts the close event, runs TryShutDownAsync() (which calls guards on each active view model), and only actually closes the window if all guards allow. This provides unsaved-changes prompts on window close without any extra manaul wiring.