Skip to content
NLNuvyntra Labs

Documentation

Navigation

UseNavigationPage or optional UseShell, UI-thread page construction, typed records, URI query, replace-root, and guards.

INavigator

INavigator is host-agnostic. ViewModels depend on the interface, never on Shell.Current or INavigation. Every public method returns Outcome and accepts CancellationToken. Current is Type? (the last ViewModel type), not an object instance.

  • NavigateToAsync<TViewModel>() — AOT-friendly typed go.
  • NavigateToAsync<TViewModel, TArgs>(TArgs) — record args; destination implements IAcceptNavArgs<T>.
  • NavigateToAsync(route, query, options) — URI path + dictionary; destination implements IAcceptNavQuery.
  • GoBackAsync, PopToRootAsync, ReplaceAsync<T>, ResetAsync<T> / ReplaceRootAsync<T>.
  • Stack, ModalStack, CanGoBack, History.

Shell and page hosts

UseNavigationPage registers MauiPageNavigator as INavigator / IPageNavigator and hops to IMainThread before new Page(). ResetAsync / ReplaceRootAsync replace window.Page with a NavigationPage. UseShell is optional. MauiVisualTree unwraps NavigationPage.CurrentPage so guards see the visible BindingContext. Register one navigator per IWindowContext.

ChooseWhen
UseNavigationPage + ResetAsync / ReplaceRootAsyncLogin → home, chat host, or any app that must drop the back-stack so Back cannot return to login. Replaces window.Page with a NavigationPage.
UseShellFlyout / tab catalog, existing Shell routes, or //home as a root ShellContent (AuthApp). ResetAsync only works when the destination is a root ShellContent.
Do not register bothUnless you really have two hosts (two windows). One INavigator per IWindowContext.
var shell = new MauiShellNavigator()
    .Map<ProductListViewModel>("//products")
    .Map<ProductDetailsViewModel>("details");

await shell.NavigateToAsync<ProductDetailsViewModel, ProductDetailsArgs>(new(42));
await shell.NavigateToAsync("details", new Dictionary<string, object> { ["ProductId"] = 42 });

IPageNavigator pages = new MauiPageNavigator(new WindowContext("main"), services)
    .Map<PageStackViewModel, PageStackPage>("stack")
    .Map<PageStackItemViewModel, PageStackItemPage>("stack-item");

await pages.NavigateToAsync("stack-item", new Dictionary<string, object> { ["Title"] = "Latte" });
if (pages.CanGoBack)
    await pages.GoBackAsync();
await pages.PopToRootAsync();

Typed args and URI query

Accept runs before InitializeAsync / OnNavigatedToAsync. Typed records are the default. URI / dictionary exists for deep links and interop — NavigationRouteTable.FormatQuery / ParseQuery / Split. There is no INavigationParameters type.

public sealed record ProductDetailsArgs(int ProductId);

public sealed class ProductDetailsViewModel : PageViewModel,
    IAcceptNavArgs<ProductDetailsArgs>, IAcceptNavQuery
{
    private int _productId;

    public void Accept(ProductDetailsArgs args) => _productId = args.ProductId;

    public void Accept(IReadOnlyDictionary<string, object> query)
    {
        if (query.TryGetValue(nameof(ProductDetailsArgs.ProductId), out var raw)
            && int.TryParse(Convert.ToString(raw), out var id))
        {
            _productId = id;
        }
    }
}

Guards and auth

Dirty forms confirm “Discard changes?” via IDialogs when DirtyNavigation is Confirm (default). Tests set DirtyNavigationMode.SilentBlock. UseAuth<TChallenge>() opens the challenge ViewModel on E_AUTH and resumes the original route after IAuthState.Changed. [RequiresAuth] / [RequiresRole] feed INavigationAuthPolicy via the generated ModuleInitializer.

Apps that must not leak a back-stack use ResetAsync / ReplaceRootAsync. On UseNavigationPage that replaces window.Page with a NavigationPage. On Shell, ResetAsync only works when //home is a root ShellContent (see AuthApp). Child ViewModels attach through IViewModelComposer.Attach. Deep-link mapping is a sample DeepLinkRouteMap — compose Plugin.Maui.DeepLinks in production. MVVMExpress does not ship Prism-style regions.