Navigation
The Navigation API is the modern successor to History, and it answers the one question the old API never could: is there anywhere to go back to? Read the session-history list, jump straight to a remembered entry, and store per-entry state without touching the URL.
@inject Bit.Butil.Navigation navigationMDN reference
NavigationManager.NavigateTo
for that. What this API adds is the ability to see the history list rather than only push onto
it. The interception half of the spec, which exists so that a router can take over navigations, is
deliberately not wrapped: a second router competing with Blazor's is not something this library should
make easy.
The reason this class exists. history.length counts the whole session including other people's sites, so it cannot distinguish a freshly opened tab from one with a page behind it - which is why an in-app back button built on History is either always enabled and sometimes does nothing, or navigates and traps the user in a loop. These two are the direct answer, and they are what a back button's disabled state should be bound to.
// exactly what an in-app back button should be:
<button disabled="@(_canGoBack is false)" @onclick="() => navigation.Back()">Back</button>
{
private bool _canGoBack;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
_canGoBack = await navigation.CanGoBack();
StateHasChanged();
}
}Every entry belonging to this document, oldest first - the same list the browser's own back and forward buttons walk. Entries from other origins are not exposed at all, which is why this needs no permission. Key identifies the slot and survives leaving and returning; Id identifies the individual visit and changes each time.
NavigationEntry[] entries = await navigation.GetEntries();
NavigationEntry? current = await navigation.GetCurrentEntry();
foreach (var entry in entries)
{
Console.WriteLine($"{entry.Index}: {entry.Url} (key {entry.Key})");
}Back and Forward move one step and return false rather than throwing when there is nowhere to go - a back button pressed at the start of the list is a normal outcome, not an exception. TraverseTo jumps straight to a remembered key however far away it is, which is how a 'back to results' button should work: not by calling Back a guessed number of times, and not by pushing a duplicate entry on top of the stack.
// remember where the user was, then return there later
var listEntry = await navigation.GetCurrentEntry();
_listKey = listEntry?.Key;
// ... the user wanders off through several pages ...
await navigation.TraverseTo(_listKey!); // straight back, one step
bool moved = await navigation.Back(); // false when there was nowhere to goState attached to a history entry survives a reload and a traversal, which makes it the right place for 'where was the user in this view' - a scroll offset, an open panel, a filter selection - as opposed to application data. UpdateCurrentEntry writes it without navigating and without adding an entry, so it is safe to call on every change; unlike History.ReplaceState it does not make you restate the URL to do so.
await navigation.UpdateCurrentEntry(new ViewState(Filter: "open", Scroll: 420));
var state = await navigation.GetCurrentState<ViewState>();
public record ViewState(string Filter, int Scroll);Write a value, then reload the page: the state comes back, and no history entry was added.
currententrychange is the one to use instead of popstate: it fires for every kind of change - a traversal, a push, a replace, an in-place state update - rather than only for traversals, and NavigationType says which it was. Navigating around this site with the buttons above, or with the browser's own back button, will drive it.
_subscription = await navigation.SubscribeCurrentEntryChange(info =>
InvokeAsync(() =>
{
_lastChange = info.NavigationType;
StateHasChanged();
}));
// later
await _subscription.DisposeAsync();API reference
ValueTask<bool> IsSupported()ValueTask<bool> CanGoBack()ValueTask<bool> CanGoForward()ValueTask<NavigationEntry?> GetCurrentEntry()ValueTask<NavigationEntry[]> GetEntries()ValueTask<T?> GetCurrentState<T>()ValueTask<bool> Back()ValueTask<bool> Forward()ValueTask<bool> TraverseTo(string key)ValueTask<bool> Navigate(string url, object? state = null, NavigationHistoryBehavior history = Auto)ValueTask<bool> Reload(object? state = null)ValueTask<bool> UpdateCurrentEntry(object? state)ValueTask<ButilSubscription> SubscribeCurrentEntryChange(Action<NavigationEventInfo>)ValueTask<ButilSubscription> SubscribeNavigateSuccess(Action<NavigationEventInfo>)ValueTask<ButilSubscription> SubscribeNavigateError(Action<NavigationEventInfo>)