shiny-maui-shell
Generate .NET MAUI Shell pages, ViewModels, navigation, and source-generated routes using Shiny MAUI Shell
ソース情報
- リポジトリ
- shinyorg/mauishell
- ソースの最終更新活動
- 2026年9月10日 03:12
- 検出された SKILL.md の言語
- 英語
- スター
- 19
- フォーク
- 2
インストール方法
デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。
ソースファイルを確認
インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。
ファイルエクスプローラー
3 ファイルSKILL.md を表示中
SKILL.md
ソースの指示 · 読み取り専用プレビュー- 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","appLinks","app link","app links","applink","deep link","deeplink","universal link","custom url scheme","url scheme","IAppLinks","UseAppLinks","AppLinkOptions","AppLinkRegistry","AppLinkRoutes","AppLinkMatch","ShinyAppLinkSchemes","ShinyAppLinkDomains","[Truncated]"]
# Shiny MAUI Shell Skill
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.
## When to Use This Skill
Invoke this skill when the user wants to:
- Create new MAUI pages with ViewModels using Shiny Shell conventions
- Set up or configure Shiny MAUI Shell in their application
- Switch between different Shell instances at runtime (e.g., login shell vs main app shell)
- Implement navigation between pages using `INavigator`
- Set or clear tab badge values on tabs in the active Shell
- Add route-based XAML navigation with `Navigate.*` attached properties
- Build multi-segment navigation chains using `INavigationBuilder` (push multiple pages, pop-and-push)
- Show dialogs (alert, confirm, prompt, action sheet) using `IDialogs`
- Add ViewModel lifecycle hooks (appearing, disappearing, navigation confirmation)
- Use source generation with `[ShellMap]` and `[ShellProperty]` attributes
- Pass parameters between pages during navigation
- Create modal pages or tab navigation
- Migrate from vanilla MAUI Shell or Prism navigation to Shiny MAUI Shell
- Set up AI-driven navigation using `Microsoft.Extensions.AI` with route discovery and `NavigateToRoute`
- Create AI-compatible ViewModels with descriptive `[ShellMap]` and `[ShellProperty]` attributes
## Library Overview
**Documentation**: 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:
- Page-to-ViewModel registration and automatic BindingContext assignment
- A testable `INavigator` service for all navigation operations
- A testable `IDialogs` service for alert, confirm, prompt, and action sheet dialogs
- `INavigationBuilder` for multi-segment navigation (push multiple pages in one operation, pop-and-push)
- Native numeric tab badges via `INavigator.SetTabBadge*` / `ClearTabBadge*`
- Attached-property XAML navigation via `Navigate.Route`, `Navigate.RelativeNavigation`, and parameter helpers
- Shell switching — swap the entire Shell at runtime (e.g., login → main app)
- ViewModel lifecycle interfaces (appearing, disappearing, dispose, navigation confirmation)
- `INavigationInterceptor` guards that can cancel or redirect any navigation - including app links, shortcuts and tab taps
- Source generators that eliminate boilerplate route registration, produce strongly-typed navigation methods, and generate AI tool metadata
- `ShinyShell` base class for deterministic initial-page BindingContext assignment
- `ShellServices` record that aggregates `INavigator`, `IDialogs`, and `IMainThread` for convenient single-parameter injection
- `IMainThread` abstraction with built-in workarounds for macOS and Linux where `MainThread.InvokeOnMainThreadAsync` can deadlock / fail
- Pluggable `IDialogs` implementation via `UseDialogs<TDialog>()` — swap in your own dialog provider (e.g. ACR UserDialogs, a custom sheet, a test double)
Inspired by [Prism Library](https://prismlibrary.com) by Dan Siegel and Brian Lagunas.
## Setup
### 1. Install NuGet Package
```bash
dotnet add package Shiny.Maui.Shell
```
### 2. Configure in MauiProgram.cs
**Manual registration:**
```csharp
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):**
```csharp
builder
.UseMauiApp<App>()
.UseShinyShell(x => x
.AddGeneratedMaps()
.AddAiTools() // registers AiMauiShellTools as singleton for AI integration
)
```
**With a custom dialog provider:**
```csharp
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.
**Built-in alternative providers** (same `IDialogs` interface — no ViewModel changes):
```csharp
// Owned, animated, themeable dialogs via Shiny.Maui.Controls (package: Shiny.Maui.Shell.ShinyDialogs)
builder
.UseMauiApp<App>()
.UseShinyControls() // registers the Controls IDialogService
.UseShinyShell(x => x
.AddGeneratedMaps()
.UseShinyDialogs() // IDialogs -> Controls dialog service
.UseShinyDialogPresenter() // ShowDialog -> overlay card (optional, see 6a)
);
// Styled UXDivers popups (package: Shiny.Maui.Shell.UxDiversDialogs)
builder
.UseMauiApp<App>()
.UseShinyShell(x => x
.AddGeneratedMaps()
.UseUxDiversDialogs() // IDialogs -> UXDivers popups
.UseUxDiversDialogPresenter() // ShowDialog -> UXDivers popup (optional, see 6a)
);
```
Either UXDivers call initializes the popup infrastructure (`UseUXDiversPopups()`) itself — do NOT
also call `builder.UseUXDiversPopups()`, and calling both Shiny extensions initializes it once.
### 3. AppShell must inherit from `ShinyShell`
Your `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:**
```xml
<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:**
```csharp
using Shiny;
namespace MyApp;
public partial class AppShell : ShinyShell
{
public AppShell()
{
InitializeComponent();
}
}
```
### Important Notes
- Pages defined in AppShell.xaml should use `registerRoute: false` since Shell already registers them
- Pages navigated to programmatically need route registration (the default behavior)
- All Pages and ViewModels are registered as Transient in DI automatically
## Code Generation Instructions
When generating code for Shiny MAUI Shell projects, follow these conventions:
### 1. ViewModels
All ViewModels must implement `INotifyPropertyChanged`. Use `CommunityToolkit.Mvvm` `ObservableObject` as the base:
```csharp
[ShellMap<MyPage>("MyRoute")]
public partial class MyViewModel : ObservableObject
{
}
```
- Use `[ShellMap<TPage>("Route")]` on every ViewModel class
- The `route` parameter must be a valid C# identifier — it is used as the generated constant name and method name
- Invalid route names (hyphens, spaces, leading digits) produce a **SHINY001** compiler error
- When no route is specified, the page type name without the `Page` suffix is used as the generated name
- Set `registerRoute: false` only for pages already declared in AppShell.xaml
- ViewModel classes using source generation should be `partial`
- Use primary constructors to inject `INavigator` and other dependencies
### 2. Navigation Properties
Use `[ShellProperty]` on ViewModel properties that should be passed as navigation parameters:
```csharp
[ShellMap<DetailPage>("Detail")]
public partial class DetailViewModel : ObservableObject
{
[ShellProperty]
public string ItemId { get; set; }
[ShellProperty(required: false)]
public int PageIndex { get; set; }
}
```
- Properties marked `[ShellProperty]` are required by default
- Use `[ShellProperty(required: false)]` for optional parameters
- `[ShellProperty]` properties are set directly by the source-generated navigation methods — no `IQueryAttributable` needed
- Source generator creates strongly-typed extension methods on `INavigator`
### 3. Lifecycle Interfaces
Implement these interfaces on ViewModels as needed:
| Interface | Purpose |
|-----------|---------|
| `IPageLifecycleAware` | `OnAppearing()` / `OnDisappearing()` hooks |
| `INavigationConfirmation` | `Task<bool> CanNavigate()` - confirm before leaving. **Only asked for user-driven Shell navigation** (tab tap, flyout, hardware back), not for `INavigator` calls - use an `INavigationInterceptor` to guard those |
| `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 |
### 4. Navigation Events
`INavigator` exposes two events for observing navigation:
- `Navigating` — fires **before** navigation with the source ViewModel instance
- `Navigated` — fires **after** navigation with the destination ViewModel instance
```csharp
navigator.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.
### 4a. Navigation Interceptors (guards)
Use `INavigationInterceptor` when navigation must be **blocked or rerouted** - auth guards, unsaved
changes, feature flags. Use the `Navigating`/`Navigated` events when you only need to observe.
```csharp
public class AuthNavigationInterceptor(IAuthService auth) : INavigationInterceptor
{
public int Order => -100; // guards before observers; ties keep registration order
public async Task<NavigationInterceptorResult> InterceptNavigationAsync(
string uri,
object? viewModel,
CancellationToken cancellationToken
)
{
if (await auth.IsAuthorized(cancellationToken) || uri.Contains("Login"))
return NavigationInterceptorResult.Continue;
return NavigationInterceptorResult.Redirect<LoginViewModel>();
}
}
```
Register in `MauiProgram.cs` - they run in registration order, first to cancel or redirect wins:
```csharp
builder.UseShinyShell(x => x
.AddGeneratedMaps()
.AddNavigationInterceptor<AuthNavigationInterceptor>()
.AddNavigationInterceptor<AuditNavigationInterceptor>()
// inline for one-liners
.AddNavigationInterceptor((uri, vm, ct) => Task.FromResult(NavigationInterceptorResult.Continue), order: 100)
);
```
**Results:**
| Result | Behaviour |
|:-------|:----------|
| `NavigationInterceptorResult.Continue` | Next interceptor, then navigate |
| `Cancel()` | Nothing navigates; the caller's `Task` completes normally |
| `Redirect("Detail")` | Push |
| `Redirect("//Main/Home")` / `Redirect("/Login")` | Reset the stack (single leading `/` is promoted to `//`) |
| `Redirect<LoginViewModel>()` | Reset the stack to that ViewModel's route - **prefer this**, it is refactor-safe |
| `Redirect<DetailViewModel>(relativeNavigation: true)` | Push that ViewModel's route |
**Rules to generate correctly:**
- The `viewModel` argument is the **destination** ViewModel, already resolved and populated
(`configure` callback run, app link values applied). Mutating it is allowed - that instance is
bound to the page, except on a `registerRoute: false` (ShellContent) page that is already
realised, which keeps the ViewModel it already has. It is `null` for unmapped routes and for Shell-driven navigation (tab taps,
hardware back), so always null-check or pattern-match: `if (viewModel is DetailViewModel vm)`.
- The ViewModel being **left** comes from `INavigationContextAccessor.Current.FromViewModel` -
inject `INavigationContextAccessor` for that, plus `FromUri`, `ToUri`, `NavigationType`,
`Parameters`, `RedirectCount`.
- A redirect re-runs the whole chain against the new URI. Guard against redirecting to the page you
are guarding (check the URI or ViewModel type first), or it just gets ignored; a real loop throws
after 10 hops.
- Interceptors cover `INavigator` calls, the navigation builder, back navigation, app links, app
shortcuts and user-driven Shell navigation. They do **not** cover `ShowDialog` or `SwitchShell`.
- Anything thrown propagates to the caller and the navigation does not happen.
- Interceptors are registered as singletons - do not hold per-navigation state in fields.
- Ordering is `Order` (lowest first) then registration order. Put guards below 0 and observers above.
- Every `INavigator` navigation method returns `Task<bool>` - false means an interceptor cancelled
it (a redirect returns true). Generated `NavigateTo{Route}` methods return `Task<bool>` too.
- To navigate **from inside** a guard, or for any navigation that must not be guarded, pass
`bypassInterceptors: true`: `NavigateTo<LoginViewModel>(bypassInterceptors: true)`,
`GoBack(1, bypassInterceptors: true)`, `PopToRoot(bypassInterceptors: true)`,
`CreateBuilder()...Navigate(bypassInterceptors: true)` or the fluent
`CreateBuilder().BypassInterceptors()...Navigate()`. A `RedirectUri` does not need it.
- `INavigationContextAccessor.Current.Direction` gives `Forward` / `Back` / `Root` when the rule
only cares which way the user is going; `.GetDirection()` converts any `NavigationType`.
- An inbound app link a guard blocks reports `AppLinkResult.Blocked` from `IAppLinks.Handle`
(distinct from `Unhandled`, which means nothing matched).
- A dialog can be awaited inside an interceptor (`IDialogs.Confirm` / `ActionSheet`) - the
navigation waits on the answer. Treat a dismissed sheet (which returns the `cancel` text) as
`Cancel()`, and always narrow to the destination being guarded first.
```csharp
// Unsaved-changes guard: uses the page being left, not the destination
public class UnsavedChangesInterceptor(
INavigationContextAccessor context,
IDialogs dialogs
) : INavigationInterceptor
{
public async Task<NavigationInterceptorResult> InterceptNavigationAsync(
string uri,
object? viewModel,
CancellationToken cancellationToken
)
{
if (context.Current?.FromViewModel is not IUnsavedChanges { HasUnsavedChanges: true })
return NavigationInterceptorResult.Continue;
return await dialogs.Confirm("Unsaved Changes", "Discard changes?")
? NavigationInterceptorResult.Continue
: NavigationInterceptorResult.Cancel();
}
}
```
```csharp
// One action sheet, all three outcomes - a dialog is awaitable here because the navigation has
// not been handed to Shell yet
public class AskFirstInterceptor(IDialogs dialogs) : INavigationInterceptor
{
const string LetItGo = "Let it through";
const string SendElsewhere = "Go to Settings instead";
const string StopIt = "Stop navigation";
public async Task<NavigationInterceptorResult> InterceptNavigationAsync(
string uri,
object? viewModel,
CancellationToken cancellationToken
)
{
if (viewModel is not DetailViewModel) // always narrow, or tab taps prompt too
return NavigationInterceptorResult.Continue;
var choice = await dialogs.ActionSheet(
$"Navigating to '{uri}'",
cancel: StopIt, // a dismissed sheet returns the cancel text
destruction: null,
buttons: [LetItGo, SendElsewhere]
);
return choice switch
{
LetItGo => NavigationInterceptorResult.Continue,
SendElsewhere => NavigationInterceptorResult.Redirect<SettingsViewModel>(relativeNavigation: true),
_ => NavigationInterceptorResult.Cancel() // cancel, dismissed or unknown
};
}
}
```
### 5. Navigation
GitHubで見るこの SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る