← Back to Component Specifications

Navigation & Routing Contract

How MenuItem.Path drives real navigation through the INavigationService abstraction (the current standard — not Shell.Current.GoToAsync directly), the Shell route table, the Path-as-URI query-property convention, glyph propagation into destination pages, and how this differs from Blazor @page routing
Work Item: AB#1821  |  Status: Draft — reverse-engineered from source, not yet reviewed  |  Peers: content contract (MenuItem & DocumentItem) · FTDT Blazor UI migration plan · N-Tier Architecture (WI-2081)  |  Tracking: WI 2238 — DotNet10 bug-fix rollup  |  Updated: 2026-08-27 08:24 ET  |  Audited: 2026-07-09 07:54 ET

First edition of this doc — nothing here existed before. A filename search for "menu" / "routing" / "query" across docs/ turned up nothing; a follow-up full-text search confirmed there was no doc for the Shell route table, the Path-as-URI-with-querystring convention, or AppShell.OnNavigating's special-casing. This is reverse-engineered directly from AppShell.xaml.cs, MenuItemViewModel.cs, and the page ViewModels that call GoToAsync. Verify against source before relying on it for planning.

Calling Shell.Current.GoToAsync directly from a ViewModel is a legacy pattern being phased out — do not copy it into new code. The current architecture standard (N-Tier Architecture Refactor, Epic WI-2081, Phase 3 WI-2085 "Service-locator removal; INavigationService; ISettingsService" — in progress) is to navigate through the injected INavigationService abstraction, either directly or via the BasePageViewModel.GoToAsync() convenience wrapper. See §4 for why, and for which call sites still need to be fixed.

Sections 1. Purpose  ·  2. Two routing systems  ·  3. Shell route table  ·  4. MenuItem.Path → standard vs. legacy  ·  5. Query properties — Path as URI  ·  6. Glyph propagation  ·  7. Blazor migration parity gap  ·  8. Open confirms

1. Purpose

The content contract documents the MenuItem data shape, including its Path field, but stops short of explaining what Path actually does at runtime. This doc is that missing piece: how a MenuItem selected in the UI turns into a real screen — the Shell route table, how Path carries a query string for special targets, and how glyph data rides along into the destination page.

2. Two routing systems — do not confuse them

This app has two independent, parallel navigation systems. A doc or an engineer reasoning about "routing" needs to say which one they mean:

Native Shell routingBlazor @page routing
Registered by Routing.RegisterRoute(name, type) calls in the AppShell constructor (ALPAMobile/AppShell.xaml.cs) @page "/route" directives at the top of each .razor file under ALPAMobile/Components/Pages/
Navigated via Standard: injected INavigationService.GoToAsync() (or the BasePageViewModel.GoToAsync() wrapper), implemented by ShellNavigationService — which itself calls Shell.Current.GoToAsync underneath, but with centralised try/catch, logging, and Sentry error reporting. Legacy: calling Shell.Current.GoToAsync(path) directly, bypassing the abstraction — see §4. Blazor's NavigationManager, inside the embedded BlazorWebView
Query params Manual: a query string appended to the route string, parsed by hand (HttpUtility.ParseQueryString) — see §5 [Parameter, SupplyParameterFromQuery] attributes, framework-parsed — used in exactly 11 places, all in Components/Pages/FTDT/*.razor
Page count ~45 pages registered (§3) 36 pages declare @page
MenuItem.Path relationship Consumed directly — see §4 Not consumed by MenuItem-driven navigation today (§7)

The two systems meet at AuthenticatedWebViewPage (AB#2591, replaced the legacy WebViewPage — a Shell-routed native page whose content is a native AutoLoginWebViewContentView/WebView, not a BlazorWebView), and at BlazorHomePage. A Shell GoToAsync can land you on a page that itself hosts Blazor routing underneath — but a MenuItem.Path targets a Shell route, never a Blazor route directly.

3. Shell route table

Every Shell-navigable page is registered once, by convention with nameof(PageType) as the route name, in the AppShell constructor:

public AppShell()
{
    InitializeComponent();
    ...
    Routing.RegisterRoute(nameof(MemberResourcesPage), typeof(MemberResourcesPage));
    Routing.RegisterRoute(nameof(AuthenticatedWebViewPage), typeof(AuthenticatedWebViewPage));
    Routing.RegisterRoute(nameof(BlazorHomePage), typeof(BlazorHomePage));
    // ...~45 registrations total
}

This table is the source of truth for valid route names — a MenuItem.Path (or any GoToAsync call) that doesn't match a registered name, or the special-cased targets in §5, will fail to navigate. It is intentionally not reproduced in full here (it would drift); read it directly at ALPAMobile/AppShell.xaml.cs.

This file (along with the rest of DotNet10's navigation/routing bug-fix history) is audited each sprint against imp/blazor-hybrid as part of the standing bug-fix rollup process — see WI 2238 for the current triage state and porting status of any open items.

One route is registered implicitly. HomePage is not explicitly registered — a code comment notes it's auto-registered by the default "Home Page" Shell item. Don't add an explicit RegisterRoute for it.

BlazorHomePage deliberately uses a fresh-instance factory, not a cached singleton — a code comment explains a cached page kept the WebView warm but confused Shell's flyout/back accounting, producing a duplicate hamburger icon. The cold-start skeleton is the accepted trade-off.

The DI-registered implementation behind every registered route is ShellNavigationService (ALPAMobile/Services/Navigation/ShellNavigationService.cs), which implements INavigationService (ALPAMobile.Application/Abstractions/Navigation/INavigationService.cs). It also owns the BUG-2073 safe-lookup workaround (Shell.Current throws InvalidOperationException during startup before Shell attaches to a Window; the service resolves the Shell instance via Application.Current.Windows instead).

4. MenuItem.Path → navigation: standard vs. legacy pattern

MenuItem.Path (see content contract §3) is a route string that ultimately reaches Shell.Current.GoToAsync — but how it gets there matters. It is consumed from several native page ViewModels, each iterating a MenuItem's children and wiring a tap command, and every one of them already has INavigationService injected in its constructor — yet most still call Shell.Current.GoToAsync directly instead of using it:

Call sitePatternContext
HomePageViewModel.cs✅ standardHome screen tiles — calls await GoToAsync(item.Path) (the BasePageViewModel wrapper, → INavigationService)
MemberResourcesPageViewModel.cs❌ legacyMember Resources category list — resolves the root MenuItem by SpecialCode, then calls Shell.Current.GoToAsync(menuItem.Path) directly on tap, despite INavigationService being injected
KCMHomePageViewModel.cs❌ legacyKCM home tiles — same direct-call pattern
MyPilotGroupPageViewModel.cs❌ legacyPilot group menu — two direct-call sites
JumpseatInfoPageViewModel.cs❌ legacyJumpseat info menu — direct call (nullable-conditional Shell.Current?.GoToAsync, also un-awaited)
AppShellViewModel.cs❌ legacyFlyout menu — top-level items and nested children, three direct-call sites

Why this matters — it already caused a crash (DOTNET-MAUI-3BK)

HomePageViewModel.cs carries this comment at its MenuItem tap-command call site, explaining why it was moved off the direct-call pattern:

// DOTNET-MAUI-3BK: Use BasePageViewModel.GoToAsync instead of Shell.Current.GoToAsync
// directly — routes are API-driven and an invalid path triggers CannotCreateAbstractClasses
// inside ActivatorUtilities.CreateInstance, crashing the app. The base helper catches and
// reports the failure while logging the route value for Sentry breadcrumb diagnosis.
await GoToAsync(item.Path);

MenuItem.Path is API-driven content, not a compile-time-checked constant — a bad or stale value from the backend is entirely possible, and a direct Shell.Current.GoToAsync call has no guard against it. ShellNavigationService.GoToAsync wraps the call in try/catch, logs the route, and reports the exception (ex.Report()) instead of letting it crash the app.

The fix already shipped once (in HomePageViewModel) but wasn't propagated. The other five call sites above have the exact same shape — an API-driven MenuItem.Path handed to Shell.Current.GoToAsync — and the same INavigationService already available via constructor injection. Fixing them is a one-line swap per call site (Shell.Current.GoToAsync(menuItem.Path)await GoToAsync(menuItem.Path) if the ViewModel derives from BasePageViewModel, else await navigationService.GoToAsync(menuItem.Path)), not a redesign. See §8.

5. Query properties — Path as a URI, not just a route name

Path is not always a bare route name. For two targets it is a full route?query=string, intercepted in AppShell.OnNavigating (ALPAMobile/AppShell.xaml.cs) before Shell would otherwise resolve it as a registered route. This is the real query-property mechanism tied to MenuItem — it is unrelated to Blazor's SupplyParameterFromQuery (§2).

Target prefixHandlingQuery params parsed
Launcher?... Cancels Shell navigation (args.Cancel()); opens the URL externally via Launcher.OpenAsync Uri — the external URL to open
WebViewPage?... Cancels Shell navigation; builds a WebViewPageViewModel via WebViewPageViewModelFactory and pushes a fresh AuthenticatedWebViewPage (AB#2591 — modal if ShowCompact/ShowFAB, otherwise a normal push) SourceString · ShowCompact · ShowFAB · BannerTitle · BannerSubtitle · BannerImage · ImageGlyph · ImageGlyphFontFamily

Both cases are parsed the same way: the target's OriginalString is rebuilt into an absolute https://alpa.org/<target> URI purely so System.Web.HttpUtility.ParseQueryString can extract the query dictionary — the constructed alpa.org URL is never actually requested.

The WebViewPage?... prefix itself is a deliberately stable wire-format tokenAB#2591 renamed the C# class this handler constructs (WebViewPageAuthenticatedWebViewPage) but left the string unchanged, since it may also arrive from server/CMS-supplied MenuItem.Path content this doc's authors don't control. Also note this query-string caller never sets a page title (only BannerTitle), and AuthenticatedWebViewPage's single header falls back BannerTitlePageTitle — a caller that sets neither renders a blank title. BannerSubtitle/BannerImage/ImageGlyph/ImageGlyphFontFamily are accepted but no longer rendered anywhere: the new header has one title slot, no subtitle or glyph slot (matching the Blazor AlpaPageTitleBar it was built to match).

No framework-level query-param declaration exists for this system — every param name above is a hand-typed string on both the producing (MenuItem.Path content, wherever it's authored) and consuming (OnNavigating) side. A typo or rename on either side fails silently (the param is just missing/empty; nothing throws). Treat the table above as the authoritative param contract until this is strongly typed.

6. Glyph propagation into the destination page

Glyphs are resolved twice, not once:

  1. For the menu tile itselfMenuItem.Glyph + GlyphFontFamily are set on MenuItemViewModel, which resolves them via GetImageFromGlyph()Helpers.GlyphHelper.GetGlyph() against the MaterialIcons or FontAwesome font-constant classes, producing a FontImageSource (size 50, color #053C89).
  2. For a WebViewPage destination — the same glyph, re-encoded as the ImageGlyph / ImageGlyphFontFamily query params (§5), is passed into the WebViewPageViewModelFactory. Since AB#2591, this no longer renders anywhere: the destination is AuthenticatedWebViewPage, whose header has no glyph slot (the legacy WebViewPage's blue title-banner did). The propagation still happens — the query param still reaches the ViewModel — it's just visually inert now.

Both paths bottom out in the same GlyphHelper + font-constants lookup — there is one glyph resolution mechanism, invoked from two call sites (one of which is now a no-op past the ViewModel). See naming decisions D3 for why glyph is modeled as data on the item rather than a separate icon-carrying type.

No glyph catalog exists. Valid glyph names are whatever constants are defined in ALPAMobile.Domain/Fonts/MaterialIcons.cs and FontAwesome.cs — there is no doc enumerating them. asset-inventory.html / foundations-reconciliation.html discuss "glyph" only for Figma vector icon assets, and flag icon-font support for the new component-spec WebView system as still open (DQ-17) — unconnected to this legacy mechanism.

7. Blazor migration parity gap

The pattern in §4 is fully implemented in the legacy native XAML layer — e.g. MemberResourcesPage.xaml + MemberResourcesPageViewModel.cs, still DI-registered (PresentationServiceCollectionExtensions.cs) and Shell-route-registered. The newer Blazor counterpart, Components/Pages/MemberResourcesPage.razor, has not reached parity: it hardcodes Link = "/member-resources" (a self-link) for every tile instead of consuming MenuItem.Path. Source comments attribute this to Blazor→native navigation being deferred (DQ-19 / #2191) — i.e. it's a known, intentional gap during migration, not a bug, but it is not yet reflected in pages/member-resources.html, which currently implies the Blazor page already routes through MenuItem.

When auditing other migrated screens for Path/routing parity, check both layers separately — a page can be "done" in native XAML and still a stub in its Blazor counterpart, or vice versa.

8. Open confirms & action items