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
INavigatorservice 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
dotnet add package Shiny.Maui.Shell
2. Configure in MauiProgram.cs
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())
Important Notes
- •The default MAUI AppShell.xaml does not need modification to work with Shiny Shell
- •Pages defined in AppShell.xaml should use
registerRoute: falsesince 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:
[ShellMap<MyPage>("myRoute")]
public partial class MyViewModel : ObservableObject
{
}
- •Use
[ShellMap<TPage>("route")]on every ViewModel class - •Set
registerRoute: falseonly for pages already declared in AppShell.xaml - •ViewModel classes using source generation should be
partial - •Use primary constructors to inject
INavigatorand other dependencies
2. Navigation Properties
Use [ShellProperty] on ViewModel properties that should be passed as navigation parameters:
[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
IQueryAttributableto receive the parameters - •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 |
INavigationAware | OnNavigatingFrom(IDictionary<string, object>) - mutate args before leaving |
IQueryAttributable | ApplyQueryAttributes(IDictionary<string, object>) - receive navigation args |
IDisposable | Cleanup when page is removed from navigation stack |
4. Navigation
Always use INavigator for navigation, never Shell.Current.GoToAsync directly:
// 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:
<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
public static class Routes
{
public const string Detail = "detail";
public const string Settings = "settings";
}
NavigationExtensions.g.cs
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
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
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
- •Use source generation - Always prefer
[ShellMap]+[ShellProperty]+AddGeneratedMaps()over manual registration - •Inject INavigator - Never use
Shell.Current.GoToAsyncdirectly; useINavigatorfor testability - •Use primary constructors - Inject dependencies via primary constructor parameters
- •Implement IQueryAttributable - Required to receive navigation parameters on the target ViewModel
- •Use ObservableObject - From CommunityToolkit.Mvvm as the ViewModel base class
- •Implement IDisposable - Clean up event handlers and subscriptions to prevent memory leaks
- •Use CanNavigate for guards - Protect unsaved changes with
INavigationConfirmation - •Mark ViewModel partial - Required when using
[ShellMap]source generation and CommunityToolkit attributes - •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
dotnet add package Shiny.Maui.Shell # Core library with source generators dotnet add package CommunityToolkit.Mvvm # ObservableObject, RelayCommand, etc.