From maui-skills
Guides .NET MAUI Shell navigation including AppShell setup, tab bars, flyout menus, GoToAsync URI navigation, route registration, query parameters, back navigation, and events.
npx claudepluginhub davidortinau/maui-skills --plugin maui-skillsThis skill uses the workspace's default tool permissions.
Always use `ContentTemplate` with `DataTemplate` so pages are created on demand.
Implements Shell navigation in .NET MAUI apps: AppShell setup, FlyoutItem/TabBar/Tabs, GoToAsync routing, parameters, back nav, events, guards. For tabs, flyouts, programmatic page navigation.
Generates .NET MAUI Shell pages, ViewModels, navigation services, source-generated routes, and dialogs using Shiny MAUI Shell. For MAUI apps with advanced Shell navigation, lifecycle hooks, and tab badges.
Guides dependency injection in .NET MAUI apps: service registration, lifetimes (Singleton/Transient/Scoped), constructor injection, Shell navigation resolution, gotchas like XAML timing and unregistered pages.
Share bugs, ideas, or general feedback.
Always use ContentTemplate with DataTemplate so pages are created on demand.
Using Content directly creates all pages during Shell init, hurting startup time.
<!-- ✅ Lazy — page created on first navigation -->
<ShellContent ContentTemplate="{DataTemplate views:HomePage}" />
<!-- ❌ Eager — page created at Shell startup -->
<ShellContent>
<views:HomePage />
</ShellContent>
IQueryAttributable gives you all parameters in one call and works on ViewModels:
public class AnimalDetailsViewModel : ObservableObject, IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("id", out var id))
AnimalId = id.ToString();
}
}
For complex objects, use ShellNavigationQueryParameters to avoid serializing:
var parameters = new ShellNavigationQueryParameters
{
{ "animal", selectedAnimal }
};
await Shell.Current.GoToAsync("animaldetails", parameters);
Use GetDeferral() for async checks (e.g., "save unsaved changes?"):
protected override async void OnNavigating(ShellNavigatingEventArgs args)
{
base.OnNavigating(args);
if (hasUnsavedChanges && args.Source == ShellNavigationSource.Pop)
{
var deferral = args.GetDeferral();
bool discard = await ShowConfirmationDialog();
if (!discard)
args.Cancel();
deferral.Complete();
}
}
Duplicate route names — Routing.RegisterRoute throws ArgumentException
if a route name is already registered or matches a visual hierarchy route.
Every route must be unique across the entire app.
Relative routes require registration — you cannot GoToAsync("somepage")
unless somepage was registered with Routing.RegisterRoute. Visual hierarchy
pages use absolute // routes instead.
Pages are created on demand — when using ContentTemplate, the page
constructor runs only on first navigation. Don't assume pages exist at startup.
Tab.Stack is read-only — you cannot manipulate the navigation stack directly;
use GoToAsync for all navigation changes.
GoToAsync is async — always await it — fire-and-forget navigation causes race conditions and can silently fail:
// ❌ Fire-and-forget — race conditions
Shell.Current.GoToAsync("details");
// ✅ Always await
await Shell.Current.GoToAsync("details");
Route hierarchy matters — absolute routes must match the full path through
the visual hierarchy (//FlyoutItem/Tab/ShellContent). Getting the path
wrong produces silent no-ops, not exceptions.