UI Tookit UI Tookit
UI Tookit UI Tookit
DocFX + Singulink = ♥

Search Results for

    Navigating

    The navigator is accessed from routed view models through this.Navigator and from the host (window or top-level control) directly as the Navigator instance. This guide covers the navigation APIs you'll use day-to-day.

    Navigating to a Concrete Route

    Parameter-less root routes can be navigated to directly:

    await this.Navigator.NavigateAsync(Routes.LoginRoot);
    

    Parameterized routes require a concrete instance created via ToConcrete:

    await this.Navigator.NavigateAsync(Routes.RepoRoot.ToConcrete("my-repo"));
    

    Multi-level routes are composed by chaining child route parts onto a root route part with Then. Each child must belong to the view model of the part before it, which the compiler enforces, and there is no limit on depth:

    await this.Navigator.NavigateAsync(
        Routes.RepoRoot.ToConcrete("my-repo")
            .Then(Routes.Repo.DocumentPage.ToConcrete(new DocumentParams { DocumentId = 42 })));
    
    await this.Navigator.NavigateAsync(
        Routes.MainRoot
            .Then(Routes.Main.Details)
            .Then(Routes.Main.Details.History));
    

    Parameterless route parts are used directly; only parameterized parts need ToConcrete.

    Note

    Before 7.0, multi-level routes were passed as separate arguments to generic overloads limited to three levels. See Upgrading to 7.0 for the old form.

    All NavigateAsync overloads accept an optional Anchor argument for URL fragments:

    await this.Navigator.NavigateAsync(Routes.HomeRoot, anchor: "about");
    

    Navigating from a URL String

    The navigator also accepts raw URL strings. This is how deep links from browsers, command-line arguments and saved links are handled:

    await _navigator.NavigateAsync("/r/my-repo/document/42");
    

    If the URL is malformed or doesn't match any registered route, a NavigationRouteException is thrown. Use try/catch around string-based navigation calls to react appropriately to malformed URLs.

    Partial Navigation

    NavigatePartialAsync swaps child routes without re-navigating the parent. This is the preferred way to switch between pages that share the same parent:

    [RelayCommand]
    private async Task ShowHomeAsync()
    {
        await this.Navigator.NavigatePartialAsync(Routes.Repo.HomePage);
    }
    

    Deeper partial routes chain child route parts with Then, starting from the child of the parent view model:

    await this.Navigator.NavigatePartialAsync(
        Routes.Repo.DocumentPage.ToConcrete(documentParams)
            .Then(Routes.Repo.DocumentPage.History));
    

    The route's generic parameters describe the parent view model the child is registered under, so the parent type is inferred from the route and never needs to be given explicitly; the navigator verifies at runtime that the current route actually contains that parent. If it doesn't, an InvalidOperationException is thrown.

    The NavigatePartialAsync(string? anchor) overload updates only the anchor on the current route. This fires the usual OnRouteNavigatingAsync / OnRouteNavigatedAsync lifecycle events, so view models that react to route changes (e.g. to update a highlighted item or scroll position) will see the new anchor. If you only want to reflect an anchor change in the URL without firing any lifecycle events, use UpdateCurrentRoute(string?) instead (see the Anchor-only update section below); the two methods are otherwise equivalent.

    Back, Forward, Refresh

    await this.Navigator.GoBackAsync();
    await this.Navigator.GoForwardAsync();
    await this.Navigator.RefreshAsync();
    

    Each of these (GoBackAsync(), GoForwardAsync(), RefreshAsync()) returns a NavigationResult (see below). Corresponding properties support binding their "can execute" state to UI:

    • CanGoBack
    • CanGoForward
    • CanRefresh
    <Button Content="Back"
            Command="{x:Bind Model.GoBackCommand}"
            IsEnabled="{x:Bind Model.Navigator.CanGoBack, Mode=OneWay}" />
    

    To check whether there is any back / forward history regardless of current navigation state:

    • HasBackHistory
    • HasForwardHistory

    Navigation Results

    Navigation methods return a NavigationResult:

    • Success: the navigation completed successfully.
    • Cancelled: the navigation was cancelled (e.g. by a view model's OnNavigatingAwayAsync setting Cancel to true).

    Callers rarely need to inspect this directly; it's useful when chaining navigations or when the caller needs to know whether a guard prevented a navigation.

    The Current Route

    CurrentRoute returns a NavigatorRoute describing the current navigation state:

    NavigatorRoute current = this.Navigator.CurrentRoute;
    
    string path = current.Path;                                  // e.g. "r/my-repo/home"
    IReadOnlyList<IConcreteRoutePart> parts = current.Parts;
    string? anchor = current.Anchor;
    
    string full = current.ToString();                            // path + query + anchor
    

    Key members include Path, Parts, Anchor, and ToString(). The latter is particularly useful for building shareable URLs:

    string shareUrl = $"{Hosts.AppBaseUrl}/{this.Navigator.CurrentRoute}";
    

    Routes obtained from the navigator are live views of its route entries, not snapshots: if the current route is later changed in place (see Updating the Current Route In-Place), a previously obtained NavigatorRoute reflects the new parts and anchor. Call ToString() if you need to capture the route at a point in time.

    Ancestor-Aware Checks

    Use these methods to branch logic based on which view models are currently in the route tree:

    if (this.Navigator.CurrentRouteHasParent<RepoViewModel>())
    {
        // currently inside a repository
    }
    

    CurrentRouteHasParent<TViewModel>() walks up the active route tree to check for a specific view model type.

    bool inRepo = this.Navigator.CurrentPathStartsWith(Routes.RepoRoot.ToConcrete("my-repo"));
    
    bool inRepoHome = this.Navigator.CurrentPathStartsWith(
        Routes.RepoRoot.ToConcrete("my-repo")
            .Then(Routes.Repo.HomePage));
    

    CurrentPathStartsWith only checks path equivalence; it does not require the current VM or view instances to match. This is useful for highlighting navigation items regardless of how the route was reached.

    GetCurrentRoutePartsToParent(Type) enumerates route parts up to a specific ancestor, which is handy when constructing breadcrumbs:

    foreach (var part in this.Navigator.GetCurrentRoutePartsToParent(typeof(MainViewModel)))
    {
        // ...
    }
    

    Navigation History

    IReadOnlyList<NavigatorRoute> backStack = this.Navigator.GetBackStack();
    IReadOnlyList<NavigatorRoute> forwardStack = this.Navigator.GetForwardStack();
    await this.Navigator.ClearHistoryAsync();
    

    The returned stacks from GetBackStack() and GetForwardStack() do not include the current route and are ordered most-recent-first. ClearHistoryAsync() wipes both stacks. Stack sizes and caching depth are configured on the navigator builder (see WinUI / Uno Setup).

    Updating the Current Route In-Place

    Sometimes you need to reflect a change in URL state without performing a navigation. Two UpdateCurrentRoute overloads support this:

    Anchor-only update

    this.Navigator.UpdateCurrentRoute(anchor: "section-2");
    

    Useful for reacting to UI state like the currently-selected item in a scrollable list. Unlike NavigatePartialAsync (see the Partial Navigation section), UpdateCurrentRoute(string?) does not fire any OnRouteNavigatingAsync / OnRouteNavigatedAsync lifecycle events; it simply updates the URL in place. That's the only difference between the two; choose UpdateCurrentRoute when the anchor change is purely cosmetic and shouldn't be observed by view models, and NavigatePartialAsync when it should.

    Replacing the leaf route part

    // After the server assigns an ID to a newly-saved entry, update the URL from
    // /entries/new to /entries/{id} without re-navigating the view model.
    this.Navigator.UpdateCurrentRoute(
        Routes.Repo.EntryPage.ToConcrete(newEntryId));
    

    UpdateCurrentRoute(IConcreteRoutePart, string?) requires the new leaf route part to map to the same view model type as the current leaf, otherwise an ArgumentException is thrown. No lifecycle methods fire; the view model and view remain mounted while the URL updates.

    Pinning a Route

    Sometimes a user needs to leave a view temporarily and come back to it with its state intact, for example to look up reference material while filling out a form. Caching alone does not guarantee that: a view model that opts out of caching is disposed as soon as it is navigated away from, and even a cached one is released once it falls outside the configured cache depth or its route drops out of the navigation stacks (e.g. when a new navigation clears the forward stack).

    PinCurrentRoute() returns a RoutePin that keeps the current route's leaf view and view model materialized until it is disposed, regardless of caching settings or navigation history. Navigate back to the route with NavigateAsync(NavigatorRoute) and the retained instances are reused:

    // Before leaving the form
    _formPin = this.Navigator.PinCurrentRoute();
    await this.Navigator.NavigateAsync(referenceRoute);
    
    // Later, from anywhere in the app
    await this.Navigator.NavigateAsync(_formPin.Route);   // same view model instance, state intact
    _formPin.Dispose();
    

    Which view models can be pinned

    A pinned view model is navigated to again on the same instance, so it must handle repeated OnNavigatedToAsync calls the way a cached view model does. CanBePinned controls this and defaults to CanBeCached, so a view model that opts out of caching keeps its guarantee of a fresh instance per activation unless it explicitly opts into pinning:

    public partial class FormViewModel : ObservableObject, IRoutedViewModel<long>
    {
        public bool CanBeCached => false;   // fresh instance per activation...
        public bool CanBePinned => true;    // ...except when pinned, which OnNavigatedToAsync handles
    
        public async Task OnNavigatedToAsync(NavigationArgs args)
        {
            if (Runner is not null)
                return; // Returning to the pinned form: state is intact, nothing to load.
    
            ...
        }
    }
    

    PinCurrentRoute() throws if the current leaf view model is not pinnable.

    Ancestors

    The pin also retains each ancestor of the leaf up to the first one that is not pinnable. Ancestors from that point up follow the normal caching rules, so the app is responsible for keeping them active while the pin matters (typically with a guard, see below). If such an ancestor is evicted and the pinned view models depended on services it provided, they are evicted with it and IsPinned becomes false; navigating to the route afterwards creates fresh instances.

    An app that needs a pinned view to survive leaving its parent context can make the parent pinnable (cacheable parents are pinnable by default). The pin then retains the parent too, regardless of cache depth.

    Points to keep in mind

    • Returning to a pinned route is an ordinary new navigation: it pushes a new history entry and fires the usual lifecycle events.
    • Route is the live route entry that was pinned (see The Current Route), so in-place updates made while it is still current are reflected in it.
    • Disposing the pin releases the retained instances on the next navigation, unless they are cached or active by then. Pins are released automatically when the navigator shuts down.
    • Always dispose pins. The navigator does not keep them alive, so a pin that is dropped without being disposed is released once it has been garbage collected rather than leaking forever, but until then its instances are retained and, worse, a pin that was never stored anywhere can be collected while the app still expects the route to be retained. Debug builds report undisposed pins.
    • Navigating to the pinned route does not prevent the user from navigating elsewhere. To stop them from leaving the reference material without returning to (or discarding) the pinned view, use a guard that inspects TargetRoute (see Guards and Redirects).

    System Back / Forward Handling

    On platforms where the OS or browser provides back / forward gestures (Android, iOS, WASM, some desktops), hook them via the WinUI navigator's HookSystemNavigationRequests() method (see WinUI / Uno Setup). Under the hood these dispatch to HandleSystemBackRequest() and HandleSystemForwardRequest() on the concrete navigator (these are host-level operations and are intentionally not part of INavigator, which view models use):

    bool handled = _navigator.HandleSystemBackRequest();
    bool handled = _navigator.HandleSystemForwardRequest();
    

    A back request returns true if any of the following happened: a dialog was dismissed, a light-dismiss popup was closed, a navigation is in progress, or a back navigation was initiated. This mirrors the convention expected by the OS: returning false allows the OS to take its default action (e.g. closing the app).

    Graceful Shutdown

    TryShutDownAsync() attempts to close down the navigator gracefully by asking each active view model if it is ready to unload:

    if (await _navigator.TryShutDownAsync())
    {
        // safe to close the window
    }
    else
    {
        // a view model requested cancellation (e.g. unsaved changes prompt)
    }
    

    See Navigation Guards and Redirects for the HookWindowClosedEvents convenience that wires this up automatically on window close.

    © Singulink. All rights reserved.