ワンクリックで
shiny-maui-shell
// Generate .NET MAUI Shell pages, ViewModels, navigation, and source-generated routes using Shiny MAUI Shell
// Generate .NET MAUI Shell pages, ViewModels, navigation, and source-generated routes using Shiny MAUI Shell
| name | shiny-maui-shell |
| description | Generate .NET MAUI Shell pages, ViewModels, navigation, and source-generated routes using Shiny MAUI Shell |
| auto_invoke | true |
| triggers | ["maui shell","shell navigation","xaml navigation","attached navigation","tab badge","badge","shell switch","switch shell","maui navigation","maui page","maui viewmodel","INavigator","IDialogs","ShellMap","ShellProperty","UseShinyShell","UseDialogs","ShinyShell","ShellServices","ShinyAppBuilder","IMainThread","Shiny.Maui.Shell","IPageLifecycleAware","INavigationConfirmation","INavigationAware","NavigateTo","GoBack","PopToRoot","SetRoot","SetTabBadge","ClearTabBadge","SwitchShell","CreateBuilder","INavigationBuilder","NavigationBuilder","Navigating","Navigated","IDialogs","Alert","Confirm","Prompt","ActionSheet","NavigationEventArgs","NavigatedEventArgs","Navigate.Route","Navigate.RelativeNavigation","Navigate.ParameterKey","Navigate.ParameterValue","Navigate.Parameters","NavigationParameters","NavigationParameter","GetGeneratedRouteInfo","GetAiToolApplicableGeneratedRoutes","NavigateToRoute","GeneratedRouteInfo","GeneratedRouteParameter","AI navigation","AI tool","chat navigation","AiMauiShellTools","AddAiTools"] |
You are an expert in Shiny MAUI Shell, a library that enhances .NET MAUI Shell with ViewModel lifecycle management, navigation services, source generation, tab badges, and XAML-triggered navigation.
Invoke this skill when the user wants to:
INavigatorNavigate.* attached propertiesINavigationBuilder (push multiple pages, pop-and-push)IDialogs[ShellMap] and [ShellProperty] attributesMicrosoft.Extensions.AI with route discovery and NavigateToRoute[ShellMap] and [ShellProperty] attributesDocumentation: https://shinylib.net/maui
GitHub: https://github.com/shinyorg/mauishell
NuGet: Shiny.Maui.Shell
Namespace: Shiny
Shiny MAUI Shell wraps .NET MAUI Shell to provide:
INavigator service for all navigation operationsIDialogs service for alert, confirm, prompt, and action sheet dialogsINavigationBuilder for multi-segment navigation (push multiple pages in one operation, pop-and-push)INavigator.SetTabBadge* / ClearTabBadge*Navigate.Route, Navigate.RelativeNavigation, and parameter helpersShinyShell base class for deterministic initial-page BindingContext assignmentShellServices record that aggregates INavigator, IDialogs, and IMainThread for convenient single-parameter injectionIMainThread abstraction with built-in workarounds for macOS and Linux where MainThread.InvokeOnMainThreadAsync can deadlock / failIDialogs implementation via UseDialogs<TDialog>() — swap in your own dialog provider (e.g. ACR UserDialogs, a custom sheet, a test double)Inspired by Prism Library by Dan Siegel and Brian Lagunas.
dotnet add package Shiny.Maui.Shell
Manual registration:
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseShinyShell(x => x
.Add<MainPage, MainViewModel>(registerRoute: false)
.Add<DetailPage, DetailViewModel>("Detail")
.Add<SettingsPage, SettingsViewModel>("Settings")
)
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
return builder.Build();
}
With source generation (preferred):
builder
.UseMauiApp<App>()
.UseShinyShell(x => x
.AddGeneratedMaps()
.AddAiTools() // registers AiMauiShellTools as singleton for AI integration
)
With a custom dialog provider:
builder
.UseMauiApp<App>()
.UseShinyShell(x => x
.AddGeneratedMaps()
.UseDialogs<MyCustomDialogs>() // register a custom IDialogs implementation
);
UseDialogs<TDialog>() replaces the default ShellDialogs provider. The default registration uses TryAddSingleton, so a UseDialogs<> call always wins.
ShinyShellYour AppShell (or any Shell subclass) must inherit from Shiny.ShinyShell instead of Shell. This ensures the initial page's BindingContext is set deterministically via Shell's own OnNavigated lifecycle.
AppShell.xaml:
<shiny:ShinyShell
x:Class="MyApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:shiny="clr-namespace:Shiny;assembly=Shiny.Maui.Shell"
xmlns:local="clr-namespace:MyApp"
Title="MyApp">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</shiny:ShinyShell>
AppShell.xaml.cs:
using Shiny;
namespace MyApp;
public partial class AppShell : ShinyShell
{
public AppShell()
{
InitializeComponent();
}
}
registerRoute: false since Shell already registers themWhen generating code for Shiny MAUI Shell projects, follow these conventions:
All ViewModels must implement INotifyPropertyChanged. Use CommunityToolkit.Mvvm ObservableObject as the base:
[ShellMap<MyPage>("MyRoute")]
public partial class MyViewModel : ObservableObject
{
}
[ShellMap<TPage>("Route")] on every ViewModel classroute parameter must be a valid C# identifier — it is used as the generated constant name and method namePage suffix is used as the generated nameregisterRoute: false only for pages already declared in AppShell.xamlpartialINavigator and other dependenciesUse [ShellProperty] on ViewModel properties that should be passed as navigation parameters:
[ShellMap<DetailPage>("Detail")]
public partial class DetailViewModel : ObservableObject
{
[ShellProperty]
public string ItemId { get; set; }
[ShellProperty(required: false)]
public int PageIndex { get; set; }
}
[ShellProperty] are required by default[ShellProperty(required: false)] for optional parameters[ShellProperty] properties are set directly by the source-generated navigation methods — no IQueryAttributable neededINavigatorImplement these interfaces on ViewModels as needed:
| Interface | Purpose |
|---|---|
IPageLifecycleAware | OnAppearing() / OnDisappearing() hooks |
INavigationConfirmation | Task<bool> CanNavigate() - confirm before leaving |
INavigationAware | OnNavigatingFrom(IDictionary<string, object>) - mutate args before leaving |
IQueryAttributable | ApplyQueryAttributes(IDictionary<string, object>) - receive navigation args (only needed for string-based NavigateTo(route, args) — not needed when using [ShellProperty]) |
IDisposable | Cleanup when page is removed from navigation stack |
INavigator exposes two events for observing navigation:
Navigating — fires before navigation with the source ViewModel instanceNavigated — fires after navigation with the destination ViewModel instancenavigator.Navigating += (sender, args) =>
{
// args.FromUri, args.FromViewModel, args.ToUri, args.NavigationType, args.Parameters
};
navigator.Navigated += (sender, args) =>
{
// args.ToUri, args.ToViewModel, args.NavigationType, args.Parameters
};
Hook these events in an IMauiInitializeService for cross-cutting concerns like logging or analytics.
Always use INavigator for navigation, never Shell.Current.GoToAsync directly:
// Route-based navigation with args
await navigator.NavigateTo("Detail", args: [("ItemId", "123"), ("PageIndex", 0)]);
// ViewModel-based navigation with strongly-typed configuration
await navigator.NavigateTo<DetailViewModel>(vm => vm.ItemId = "123");
// Source-generated strongly-typed method (preferred)
await navigator.NavigateToDetail("123", pageIndex: 0);
// Absolute navigation (navigates to root route "//Detail")
await navigator.NavigateTo("Detail", relativeNavigation: false);
await navigator.NavigateTo<DetailViewModel>(relativeNavigation: false);
// Go back with result parameters
await navigator.GoBack(("Result", selectedItem));
// Go back multiple pages
await navigator.GoBack(backCount: 2);
// Pop to root
await navigator.PopToRoot();
// Switch to a different Shell instance
await navigator.SwitchShell(new MainAppShell());
// Switch to a Shell resolved from DI
await navigator.SwitchShell<MainAppShell>();
Use INavigationBuilder for multi-segment navigation (pushing multiple pages in a single operation):
// Push a chain of pages: One > Another > Two
await navigator
.CreateBuilder()
.Add<OneViewModel>(x => x.Text = "First")
.Add<AnotherViewModel>(x => x.Arg = "Middle")
.Add<TwoViewModel>(x => x.Text = "Last")
.Navigate();
// Pop back 2 pages, then push a new page
await navigator
.CreateBuilder()
.PopBack(2)
.Add<OneViewModel>(x => x.Text = "Replaced")
.Navigate();
// Add by route name (no configure callback)
await navigator
.CreateBuilder()
.Add("Detail")
.Navigate();
Important Shell constraints for the Navigation Builder:
Routing.RegisterRoute (i.e., registerRoute: true, which is the default). Pages declared as ShellContent in XAML cannot be used in multi-segment relative URIs.PopBack() must be called before any Add() calls.fromRoot: true on CreateBuilder only works when the target route is a shell-declared route (a ShellContent in XAML), not a globally registered route.Use the badge APIs when a route already exists as a tab in the active Shell:
// Route-based badge updates
await navigator.SetTabBadge("Inbox", 3);
await navigator.ClearTabBadge("Inbox");
// ViewModel-based badge updates
await navigator.SetTabBadge<InboxViewModel>(7);
await navigator.ClearTabBadge<InboxViewModel>();
PlatformNotSupportedException (neutral target, Linux, macOS AppKit)Use Navigate.* attached properties for simple route-based navigation directly from XAML:
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:shiny="clr-namespace:Shiny;assembly=Shiny.Maui.Shell">
<ContentPage.ToolbarItems>
<ToolbarItem Text="Home"
shiny:Navigate.Route="MainPage"
shiny:Navigate.RelativeNavigation="False" />
</ContentPage.ToolbarItems>
<Button Text="Open Detail"
shiny:Navigate.Route="Detail"
shiny:Navigate.ParameterKey="ItemId"
shiny:Navigate.ParameterValue="{Binding SelectedId}" />
</ContentPage>
For multiple parameters:
<Button Text="Open Modal"
shiny:Navigate.Route="Modal">
<shiny:Navigate.Parameters>
<shiny:NavigationParameters>
<shiny:NavigationParameter Key="Arg1" Value="{Binding NavArg}" />
<shiny:NavigationParameter Key="Arg2" Value="5" />
</shiny:NavigationParameters>
</shiny:Navigate.Parameters>
</Button>
Button, MenuItem, ToolbarItemNavigate.Route accepts the route string passed to INavigator.NavigateTo(...)Shiny MAUI Shell generates AI-compatible route metadata and navigation methods for use with Microsoft.Extensions.AI. An AI chat client can discover routes, understand their purpose, and navigate with parameters extracted from natural language.
Describe routes for AI — Add description to [ShellMap] and [ShellProperty]:
public enum WorkOrderPriority { Low, Medium, High, Urgent }
[ShellMap<WorkOrderPage>(description: "Use when the user reports something broken or needing repair")]
public partial class WorkOrderViewModel : ObservableObject
{
[ShellProperty("Summarize what is broken based on what the user said", required: true)]
public string Description { get; set; } = string.Empty;
[ShellProperty("Infer urgency from tone. Must be: Low, Medium, High, or Urgent", required: true)]
public WorkOrderPriority Priority { get; set; } = WorkOrderPriority.Medium;
}
Generated AI class (AiMauiShellTools):
The source generator produces an AiMauiShellTools class (name configurable via ShinyMauiShell_AiToolsClassName) that takes INavigator via constructor injection and provides:
Prompt — pre-formatted string describing all AI-applicable routes for seeding AI system messagesTools — ready-to-use AITool[] instances for route discovery and navigationGetAiToolApplicableGeneratedRoutes() — returns only routes that have a description AND at least one parameterNavigateToRoute(route, args) — AI-friendly navigation using switch dispatch to NavigateTo<TViewModel> with direct property setters and automatic type conversion (int, bool, double, enums, DateTime, etc.)Additionally, GetGeneratedRouteInfo() remains as a static extension on INavigator returning all routes with parameter metadata.
A generated AddAiTools() extension on ShinyAppBuilder registers the class as a singleton.
Wire up AI tools (enabled by default when Microsoft.Extensions.AI is referenced):
// MauiProgram.cs
builder.UseShinyShell(x => x
.AddGeneratedMaps()
.AddAiTools()
);
// In your ViewModel — inject AiMauiShellTools
public class ChatViewModel(AiMauiShellTools aiTools)
{
var options = new ChatOptions { Tools = [.. aiTools.Tools] };
}
Key conventions for AI-friendly ViewModels:
GetAiToolApplicableGeneratedRoutes (not GetGeneratedRouteInfo) to keep the AI focused on actionable routesstring, int, bool, double, enums, DateTime, Guid, etc. — the generated NavigateToRoute handles type conversion automaticallyAlways use IDialogs for user-facing dialogs. Inject it via the primary constructor:
public class MyViewModel(INavigator navigator, IDialogs dialogs)
{
// Alert - informational message
await dialogs.Alert("Title", "Something happened");
// Confirm - yes/no question, returns bool
bool confirmed = await dialogs.Confirm("Delete?", "Are you sure?");
// Prompt - text input, returns string? (null if cancelled)
var name = await dialogs.Prompt("Name", "Enter your name", placeholder: "John Doe");
// Prompt with numeric keyboard
var age = await dialogs.Prompt("Age", "Enter your age", keyboard: Keyboard.Numeric);
// Action sheet - choose from options
var choice = await dialogs.ActionSheet("Options", "Cancel", "Delete", "Edit", "Share");
}
ShellServices is a convenience record that bundles the three shell services together. Inject it when a ViewModel or service needs most of them and you want a single parameter:
public record ShellServices(
INavigator Navigator,
IDialogs Dialogs,
IMainThread MainThread
);
public class MyViewModel(ShellServices shell)
{
async Task DoWork()
{
shell.MainThread.BeginInvokeOnMainThread(() => /* UI update */);
await shell.Dialogs.Alert("Done", "Work complete");
await shell.Navigator.GoBack();
}
}
IMainThread is the thread-marshalling abstraction used internally by ShellNavigator and ShellDialogs. Prefer it over Microsoft.Maui.ApplicationModel.MainThread inside Shiny Shell code because the default implementation (MauiMainThread) transparently works around platforms where MAUI's MainThread.InvokeOnMainThreadAsync is broken — currently macOS and Linux, where calls are executed inline instead of being dispatched.
public interface IMainThread
{
Task InvokeOnMainThreadAsync(Action action);
Task InvokeOnMainThreadAsync(Func<Task> func);
Task<T> InvokeOnMainThreadAsync<T>(Func<Task<T>> func);
void BeginInvokeOnMainThread(Action action);
}
Both ShellServices and IMainThread are registered as singletons by UseShinyShell() — no extra setup required.
Set Shell.PresentationMode="Modal" on the page XAML:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Shell.PresentationMode="Modal"
x:Class="MyApp.ModalPage">
Navigate to it like any other page. Close with GoBack().
Place files following standard MAUI conventions:
Views/{Name}Page.xaml + Views/{Name}Page.xaml.csViewModels/{Name}ViewModel.csFeatures/{Feature}/{Name}Page.xaml + {Name}ViewModel.csThe source generator produces up to three files from [ShellMap] and [ShellProperty] attributes. Each can be individually disabled via MSBuild properties.
The constant name is derived from the route parameter (or page type name without Page suffix when no route is specified):
public static class Routes
{
public const string Detail = "Detail";
public const string Settings = "Settings";
}
Method names are also derived from the route parameter:
public static class NavigationExtensions
{
public static Task NavigateToDetail(this INavigator navigator, string itemId, int pageIndex = default)
{
return navigator.NavigateTo<DetailViewModel>(x =>
{
x.ItemId = itemId;
x.PageIndex = pageIndex;
});
}
}
Uses inline string literals (not Routes.* constants), so it works regardless of whether route constants are enabled:
public static class NavigationBuilderExtensions
{
public static ShinyAppBuilder AddGeneratedMaps(this ShinyAppBuilder builder)
{
builder.Add<DetailPage, DetailViewModel>("Detail");
builder.Add<SettingsPage, SettingsViewModel>("Settings");
return builder;
}
}
Disable individual generated files via MSBuild properties in .csproj:
<PropertyGroup>
<!-- Disable Routes.g.cs -->
<ShinyMauiShell_GenerateRouteConstants>false</ShinyMauiShell_GenerateRouteConstants>
<!-- Disable NavigationExtensions.g.cs -->
<ShinyMauiShell_GenerateNavExtensions>false</ShinyMauiShell_GenerateNavExtensions>
<!-- Disable AI extensions (enabled by default, requires Microsoft.Extensions.AI) -->
<ShinyMauiShell_GenerateAiExtensions>false</ShinyMauiShell_GenerateAiExtensions>
<!-- Customize the generated AI tools class name (default: AiMauiShellTools) -->
<ShinyMauiShell_AiToolsClassName>MyAppAiTools</ShinyMauiShell_AiToolsClassName>
<!-- Customize the generated static extensions class name (default: AiExtensions) -->
<ShinyMauiShell_AiExtensionsClassName>MyAppRouteExtensions</ShinyMauiShell_AiExtensionsClassName>
<!-- Customize the AI navigate method name (default: NavigateToRoute) -->
<ShinyMauiShell_AiNavigateMethodName>GoToPage</ShinyMauiShell_AiNavigateMethodName>
</PropertyGroup>
| Property | Default | Controls |
|---|---|---|
ShinyMauiShell_GenerateRouteConstants | true | Routes.g.cs |
ShinyMauiShell_GenerateNavExtensions | true | All navigation extensions and AddGeneratedMaps |
ShinyMauiShell_GenerateAiExtensions | true | AiMauiShellTools class, AddAiTools(), GetAiToolApplicableGeneratedRoutes, NavigateToRoute, and Prompt. Requires Microsoft.Extensions.AI (SHINY003 error if missing). Set to false to disable |
ShinyMauiShell_AiToolsClassName | AiMauiShellTools | Class name for the generated AI tools class |
ShinyMauiShell_AiExtensionsClassName | AiExtensions | Class name for the static route info extensions class |
ShinyMauiShell_AiNavigateMethodName | NavigateToRoute | Method name for the AI-friendly navigate method |
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Shiny;
namespace MyApp.ViewModels;
[ShellMap<DetailPage>("Detail")]
public partial class DetailViewModel(INavigator navigator, IDialogs dialogs) : ObservableObject,
IPageLifecycleAware,
INavigationConfirmation,
INavigationAware,
IDisposable
{
[ShellProperty]
[ObservableProperty]
string itemId;
[ObservableProperty]
string title;
bool hasUnsavedChanges;
// Page appeared
public void OnAppearing()
{
// Load data, start listening, etc.
}
// Page disappearing
public void OnDisappearing()
{
// Pause operations
}
// Confirm before leaving
public async Task<bool> CanNavigate()
{
if (!hasUnsavedChanges)
return true;
return await dialogs.Confirm(
"Unsaved Changes",
"You have unsaved changes. Discard them?"
);
}
// Mutate parameters before leaving
public void OnNavigatingFrom(IDictionary<string, object> parameters)
{
parameters["LastViewedItem"] = ItemId;
}
[RelayCommand]
async Task Save()
{
// Save logic
hasUnsavedChanges = false;
await navigator.GoBack(("Saved", true));
}
[RelayCommand]
Task GoBack() => navigator.GoBack();
public void Dispose()
{
// Cleanup subscriptions, timers, etc.
}
}
[ShellMap] + [ShellProperty] + AddGeneratedMaps() over manual registrationShell.Current.GoToAsync directly; use INavigator for testabilityShell.Current.DisplayAlert directly; use IDialogs for testability[ShellProperty] - Properties are set directly by generated navigation methods — no IQueryAttributable neededINavigationConfirmation[ShellMap] source generation and CommunityToolkit attributesNavigate.* for lightweight XAML wiring - Prefer ViewModel commands when navigation needs branching logic or validationFor detailed templates and examples, see:
reference/templates.md - Page and ViewModel code generation templatesreference/api-reference.md - Full API surface, interfaces, and attributesdotnet add package Shiny.Maui.Shell # Core library with source generators
dotnet add package CommunityToolkit.Mvvm # ObservableObject, RelayCommand, etc.