AgentSkillsCN

shiny-maui-shell

为 .NET MAUI Shell 项目生成页面、ViewModel、导航,以及通过源码生成的路由——使用 Shiny MAUI Shell。

SKILL.md
--- frontmatter
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
  - maui navigation
  - maui page
  - maui viewmodel
  - INavigator
  - ShellMap
  - ShellProperty
  - UseShinyShell
  - ShinyAppBuilder
  - Shiny.Maui.Shell
  - IPageLifecycleAware
  - INavigationConfirmation
  - INavigationAware
  - NavigateTo
  - GoBack
  - PopToRoot
  - SetRoot

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, and source generation.

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
  • Implement navigation between pages using INavigator
  • 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

Library Overview

Documentation: https://shinylib.net/maui 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
  • ViewModel lifecycle interfaces (appearing, disappearing, dispose, navigation confirmation)
  • Source generators that eliminate boilerplate route registration and produce strongly-typed navigation methods
  • No special AppShell subclass required

Inspired by Prism Library 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())

Important Notes

  • The default MAUI AppShell.xaml does not need modification to work with Shiny Shell
  • 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
  • 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, IQueryAttributable
{
    [ShellProperty]
    public string ItemId { get; set; }

    [ShellProperty(required: false)]
    public int PageIndex { get; set; }

    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.TryGetValue(nameof(ItemId), out var id))
            ItemId = id?.ToString();
    }
}
  • Properties marked [ShellProperty] are required by default
  • Use [ShellProperty(required: false)] for optional parameters
  • The ViewModel must implement IQueryAttributable to receive the parameters
  • Source generator creates strongly-typed extension methods on INavigator

3. Lifecycle Interfaces

Implement these interfaces on ViewModels as needed:

InterfacePurpose
IPageLifecycleAwareOnAppearing() / OnDisappearing() hooks
INavigationConfirmationTask<bool> CanNavigate() - confirm before leaving
INavigationAwareOnNavigatingFrom(IDictionary<string, object>) - mutate args before leaving
IQueryAttributableApplyQueryAttributes(IDictionary<string, object>) - receive navigation args
IDisposableCleanup when page is removed from navigation stack

4. Navigation

Always use INavigator for navigation, never Shell.Current.GoToAsync directly:

csharp
// Route-based navigation with args
await navigator.NavigateTo("detail", ("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);

// 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();

// Set new root page
await navigator.SetRoot<MainViewModel>();

// Dialogs
await navigator.Alert("Title", "Something happened");
bool confirmed = await navigator.Confirm("Delete?", "Are you sure?");

5. Modal Pages

Set Shell.PresentationMode="Modal" on the page XAML:

xml
<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().

6. File Organization

Place files following standard MAUI conventions:

  • Pages: Views/{Name}Page.xaml + Views/{Name}Page.xaml.cs
  • ViewModels: ViewModels/{Name}ViewModel.cs
  • Or co-locate: Features/{Feature}/{Name}Page.xaml + {Name}ViewModel.cs

Source Generation Output

The source generator produces three files from [ShellMap] and [ShellProperty] attributes:

Routes.g.cs

csharp
public static class Routes
{
    public const string Detail = "detail";
    public const string Settings = "settings";
}

NavigationExtensions.g.cs

csharp
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;
        });
    }
}

NavigationBuilderExtensions.g.cs

csharp
public static class NavigationBuilderExtensions
{
    public static ShinyAppBuilder AddGeneratedMaps(this ShinyAppBuilder builder)
    {
        builder.Add<DetailPage, DetailViewModel>(Routes.Detail);
        builder.Add<SettingsPage, SettingsViewModel>(Routes.Settings);
        return builder;
    }
}

Complete ViewModel Example

csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Shiny;

namespace MyApp.ViewModels;

[ShellMap<DetailPage>("detail")]
public partial class DetailViewModel(INavigator navigator) : ObservableObject,
    IQueryAttributable,
    IPageLifecycleAware,
    INavigationConfirmation,
    INavigationAware,
    IDisposable
{
    [ShellProperty]
    [ObservableProperty]
    string itemId;

    [ObservableProperty]
    string title;

    bool hasUnsavedChanges;

    // Receive navigation parameters
    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.TryGetValue(nameof(ItemId), out var id))
            ItemId = id?.ToString();
    }

    // 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 navigator.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.
    }
}

Best Practices

  1. Use source generation - Always prefer [ShellMap] + [ShellProperty] + AddGeneratedMaps() over manual registration
  2. Inject INavigator - Never use Shell.Current.GoToAsync directly; use INavigator for testability
  3. Use primary constructors - Inject dependencies via primary constructor parameters
  4. Implement IQueryAttributable - Required to receive navigation parameters on the target ViewModel
  5. Use ObservableObject - From CommunityToolkit.Mvvm as the ViewModel base class
  6. Implement IDisposable - Clean up event handlers and subscriptions to prevent memory leaks
  7. Use CanNavigate for guards - Protect unsaved changes with INavigationConfirmation
  8. Mark ViewModel partial - Required when using [ShellMap] source generation and CommunityToolkit attributes
  9. Pass results via GoBack args - Return data to the previous page through navigation parameters

Reference Files

For detailed templates and examples, see:

  • reference/templates.md - Page and ViewModel code generation templates
  • reference/api-reference.md - Full API surface, interfaces, and attributes

Common Packages

bash
dotnet add package Shiny.Maui.Shell           # Core library with source generators
dotnet add package CommunityToolkit.Mvvm      # ObservableObject, RelayCommand, etc.