@inject IBrouter brouter @inject DemoState demoState @implements IDisposable @code { private int _currentCount; private int _dashboardVisits; private Func? _onNavigatingHandler; void ChangeCountValue(int value) { _currentCount = value; StateHasChanged(); } protected override void OnInitialized() { base.OnInitialized(); _onNavigatingHandler = ctx => { // analytics, telemetry, page title, ... return ValueTask.CompletedTask; }; brouter.OnNavigating += _onNavigatingHandler; } public void Dispose() { if (_onNavigatingHandler is not null) { brouter.OnNavigating -= _onNavigatingHandler; _onNavigatingHandler = null; } } private ValueTask GuardEvenSecond(BrouterNavigationContext ctx) { if (DateTime.Now.Second % 2 != 0) ctx.Redirect("/403"); return ValueTask.CompletedTask; } private async ValueTask CheckEvenMinute(BrouterNavigationContext ctx) { await Task.Delay(1, ctx.CancellationToken); if (DateTime.Now.Minute % 2 != 0) { ctx.Redirect("/403"); } } // ===== data-layer demos (see DataPage / DeferredPage / UnstablePage) ===== // Deliberately slow so the router-level pending UI is visible, and so the // StaleTime cache / link preloading effects are obvious (instant when warm). private async ValueTask LoadData(BrouterNavigationContext ctx) { await Task.Delay(600, ctx.CancellationToken); return new LoadedInfo(DateTime.Now, ctx.IsRevalidation, ctx.To.GetQuery("page") ?? "1"); } // Deferred data: only the summary blocks navigation; the details task is returned UNAWAITED // and streams into after the page has revealed. private async ValueTask LoadDeferred(BrouterNavigationContext ctx) { await Task.Delay(300, ctx.CancellationToken); return new DeferredReport($"Report generated at {DateTime.Now:HH:mm:ss}.", LoadSlowDetailsAsync()); } private static async Task LoadSlowDetailsAsync() { await Task.Delay(1500); return ["42 orders in the last hour", "7 new signups", "0 incidents"]; } // Error-boundary demo: throws while DemoState.UnstableShouldFail is armed; the route's // ErrorContent renders the failure with a heal-and-retry button. private async ValueTask LoadUnstable(BrouterNavigationContext ctx) { await Task.Delay(300, ctx.CancellationToken); if (demoState.UnstableShouldFail) throw new InvalidOperationException("The upstream service exploded (simulated)."); return $"Fresh data loaded at {DateTime.Now:HH:mm:ss}."; } // ===== guards for the demos ===== // Leave guard for /editor: while the editor is dirty, cancel preventively - the URL never moves. private ValueTask GuardEditorLeave(BrouterNavigationContext ctx) { if (demoState.IsEditorDirty) ctx.Cancel(); return ValueTask.CompletedTask; } // /blocked always cancels - the OutcomesPage uses it to show a Cancelled NavigateAsync result. private ValueTask BlockAlways(BrouterNavigationContext ctx) { ctx.Cancel(); return ValueTask.CompletedTask; } // Shared guard on the dashboard's pathless group: one declaration covers every child. private ValueTask CountDashboardVisit(BrouterNavigationContext ctx) { _dashboardVisits++; return ValueTask.CompletedTask; } // Lazy route loading hook. This demo has nothing to load lazily, but the wiring shows the // shape: on WebAssembly you would await LazyAssemblyLoader.LoadAssembliesAsync(...) based on // ctx.To.Path and return the assemblies - their @page components are registered within the // SAME navigation, so a deep link into a lazy area just works. private ValueTask?> OnNavigate(BrouterNavigationContext ctx) => ValueTask.FromResult?>(null); } @* AppAssembly enables attribute-route discovery: components with @page / [Route] (e.g. DiscoveredPage) are matched alongside the hand-declared routes below, without being listed here. OnNavigateAsync is the lazy route-loading hook (see OnNavigate above). *@ @*============================================================================*@ @* The site itself: landing page, docs hub (a nested-route tree whose parent renders the sidebar shell and whose children fill its outlet), and the playground index. *@ @* Empty path = the index route: it matches /docs exactly. *@ @* ParametersPage receives the host's shared count to demonstrate host-state sharing through . *@ @* Redirects keep the site's previous flat URLs working (and dogfood RedirectTo). *@ @*============================================================================*@ @* Live playground routes: real, clickable proofs for the docs pages above. *@ @* Optional parameter: /profile or /profile/saleh both match. *@ @* Catch-all parameter: /posts/2024/12/hello-world binds the whole tail. *@

Test2 pattern

Matched {prefix}/test2/{postfix}.

prefix
@p["prefix"]
postfix
@p["postfix"]
@* Wildcard route - only wins if nothing more specific matches. *@

Wildcard test

Matched the wildcard pattern /*/test.

@*============================================================================*@ @* Framework-parity template features - the route-templates docs page documents every shape with code samples and links into the live routes below: middle optionals (ProductsListPage), constrained middle optionals (WelcomePage), default values (BlogPage), complex segments (FileDetailPage / ApiVersionPage), and a constrained single-star catch-all (AssetsPage). *@ @* Middle optional: parses like the built-in router; required at match time, so /products/en/list matches but /products/list does not. *@ @* Constrained middle optional: nonfile rejects first segments that look like file names. *@ @* Default value: /blog binds action = "Index"; /blog/archive overrides it. *@ @* Complex segments: several parameters inside ONE URL segment, matched right-to-left. *@ @* Constrained catch-all ({*path} == {**path} for matching): the constraint validates the whole remainder; nonfile also accepts the empty remainder, so /assets itself matches. *@ @* Constraint chaining: validators gate the match, the last TYPE constraint (int) binds. *@

Rated @p["score"] / 5

Matched /rate/{score:int:min(1):max(5)} - min/max validated the value, and it bound as @(p["score"]?.GetType().Name) thanks to :int.

/rate/5 /rate/9 → 404 ← All template shapes
@*============================================================================*@ @* Interactive constraint tester (ConstraintsPage, under /docs/constraints). One constrained route per catalog entry plus one unconstrained fallback: both can match /c/x/y, and specificity picks the winner - the fallback only renders when the constrained sibling rejected the value. *@ @foreach (var c in ConstraintCatalog.All) { } @*============================================================================*@

Nested route /{id:int}/hello

id
@p["id"]

Nested route /{id:int}/world

id
@p["id"]

@*============================================================================*@

Guarded route /g1

Allowed only when the current second is even - otherwise redirects to /403.

access granted

You made it through the guard.

Guarded route /g2

Allowed only when the current minute is even - otherwise redirects to /403.

access granted

You made it through the async guard.

@*============================================================================*@

Child: /nested/n1

count
@p["count"]

Child: /nested2/n1

count
@p["count"]

Child: /nested2/n2

count
@p["count"]

Nested route /nested3

An inline parent route that also defines its own outlet.

Children

Outlet

Child: /nested3/n1

count
@p["count"]

Child: /nested3/n2

count
@p["count"]
@*============================================================================*@

404

Nothing matched this address - no declared route, and no @page component either.

Back to home Documentation Playground

Looking for something in particular? Press Ctrl K to search.

403

A guard turned this navigation away - the URL never committed.

Back to home How guards work
@*============================================================================*@ @* Data layer: loader + StaleTime cache + revalidation + query updates (DataPage), deferred/streamed data (DeferredPage), error boundary with retry (UnstablePage). *@

Route failed to load

@err.Exception.Message

@*============================================================================*@ @* Navigation control: leave guard (unsaved changes), history entry state, awaited navigation outcomes, and the always-cancelling /blocked target. *@ @* Component-level navigation lock: the PAGE holds the veto (OnDeactivating/OnRenavigating on BrouterRouteBase) and shows a custom confirmation dialog - including for /lock/1 -> /lock/2 parameter changes, which the route-declared LeaveGuard above can never see. *@
This never renders - the guard always cancels.
@*============================================================================*@ @* Keep-alive: /sticky vs the /fleeting control shows singleton retention plus the route lifecycle (StickyNotePage derives from BrouterRouteBase and pauses its timer in OnDeactivated / resumes it in OnActivated); /notes/{id} shows per-parameter caching (KeepAliveMax) with LRU eviction, plus the resets: IBrouter.ClearKeepAlive(), ClearKeepAlive(includeActive: true) and ReloadAsync(). *@ @*============================================================================*@ @* Route lifecycle: /lifecycle/{id} is a singleton Component route whose page implements IBrouterRoute directly (auto-discovered by the router - no base class needed). The same instance is re-committed across id/query changes (OnRenavigated); leaving fires OnDeactivated with reason Disposing. Contrast with /sticky above, where keep-alive retention makes the reason Hidden. *@ @*============================================================================*@ @* View Transitions showcase: shared-element morphs between the gallery grid and the item detail hero via matching view-transition-name declarations (pure CSS wiring). *@ @*============================================================================*@ @* Named outlets + pathless group: one child route fills two layout regions; the group attaches a shared guard to every dashboard child without adding a URL segment. *@

Dashboard (named outlets)

One child route drives two regions. Entries into this section, counted by the pathless group's shared guard: @_dashboardVisits

Main (primary outlet)

Sidebar (named outlet)

Stats main content, rendered by the primary outlet.

Stats filters - a <BrouterView Name="sidebar"> rendered by the same-named outlet.

Activity main content. This route declares no sidebar view, so the named outlet stays empty.

@* Shown while a navigation awaits slow loaders (visible on the first, uncached visit to /data). *@
Loading route data…
@* Router-level error boundary: failures on routes without their own ErrorContent land here. *@

Navigation failed

@err.Exception.Message