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 routingFirst 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.
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.
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 routing | Blazor @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.
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).
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 site | Pattern | Context |
|---|---|---|
HomePageViewModel.cs | ✅ standard | Home screen tiles — calls await GoToAsync(item.Path) (the BasePageViewModel wrapper, → INavigationService) |
MemberResourcesPageViewModel.cs | ❌ legacy | Member 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 | ❌ legacy | KCM home tiles — same direct-call pattern |
MyPilotGroupPageViewModel.cs | ❌ legacy | Pilot group menu — two direct-call sites |
JumpseatInfoPageViewModel.cs | ❌ legacy | Jumpseat info menu — direct call (nullable-conditional Shell.Current?.GoToAsync, also un-awaited) |
AppShellViewModel.cs | ❌ legacy | Flyout menu — top-level items and nested children, three direct-call sites |
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.
Path as a URI, not just a route namePath 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 prefix | Handling | Query 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 token —
AB#2591 renamed the C# class this handler constructs (WebViewPage → AuthenticatedWebViewPage)
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
BannerTitle → PageTitle — 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.
Glyphs are resolved twice, not once:
MenuItem.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).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.
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.
Shell.Current.GoToAsync call sites (§4) — highest priority,
lowest effort: MemberResourcesPageViewModel, KCMHomePageViewModel,
MyPilotGroupPageViewModel (×2), JumpseatInfoPageViewModel,
AppShellViewModel (×3). Each already has INavigationService injected; this closes
the same crash exposure DOTNET-MAUI-3BK fixed in HomePageViewModel, and moves
the app closer to WI-2085 (service-locator removal).INavigationService/Shell.Current.GoToAsync for MenuItem.Path
targets once the migration reaches that screen.pages/member-resources.html accuracy — should be corrected to describe the
current self-link behavior, or updated once the Blazor page is wired to MenuItem.Path.