Skip to main content

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で見る