Shiny.Maui.Shell
5.0.0
Prefix Reserved
See the version list below for details.
dotnet add package Shiny.Maui.Shell --version 5.0.0
NuGet\Install-Package Shiny.Maui.Shell -Version 5.0.0
<PackageReference Include="Shiny.Maui.Shell" Version="5.0.0" />
<PackageVersion Include="Shiny.Maui.Shell" Version="5.0.0" />
<PackageReference Include="Shiny.Maui.Shell" />
paket add Shiny.Maui.Shell --version 5.0.0
#r "nuget: Shiny.Maui.Shell, 5.0.0"
#:package Shiny.Maui.Shell@5.0.0
#addin nuget:?package=Shiny.Maui.Shell&version=5.0.0
#tool nuget:?package=Shiny.Maui.Shell&version=5.0.0
Shiny MAUI Shell
Make .NET MAUI Shell shinier with ViewModel lifecycle management, navigation services, and source generation to remove boilerplate, reduce errors, and make your app testable.
Inspired by Prism Library by Dan Siegel and Brian Lagunas.
Features
π§ Navigation β INavigator
| Capability | Description |
|---|---|
| Route-based | NavigateTo("Detail", args: [("Id", "123")]) |
| ViewModel-based | NavigateTo<DetailViewModel>(vm => vm.Id = "123") |
| Source-generated | NavigateToDetail("123") β zero guesswork |
| GoBack | Single page, multi-page GoBack(3), or PopToRoot() |
| Root navigation | NavigateTo<DashboardViewModel>(relativeNavigation: false) β reset the stack |
| Navigation builder | Fluent multi-segment: CreateBuilder().AddDetail(42).AddModal().Navigate() |
| Shell switching | SwitchShell(new MainShell()) or SwitchShell<TShell>() via DI |
| Tab badges | Numeric tab badges via route or ViewModel β SetTabBadge<InboxViewModel>(3) |
| XAML navigation | Attached properties on Button, MenuItem, and ToolbarItem |
π¬ Dialogs β IDialogs
| Method | Returns |
|---|---|
Alert(title, message) |
Task |
Confirm(title, message) |
Task<bool> |
Prompt(title, message) |
Task<string?> |
ActionSheet(title, cancel, destructive, ...buttons) |
Task<string> |
Thread-safe β dispatches to UI thread automatically. Inject separately from
INavigatorfor clean separation of concerns.Alternative provider: Use
Shiny.Maui.Shell.UxDiversDialogsfor styled popup dialogs powered by UXDivers Popups β sameIDialogsinterface, no ViewModel changes needed.
π‘ Navigation Events
| Event | Fires | Key Properties |
|---|---|---|
Navigating |
Before navigation | FromUri Β· FromViewModel Β· ToUri Β· NavigationType Β· Parameters |
Navigated |
After page resolves | ToUri Β· ToViewModel Β· NavigationType Β· Parameters |
NavigationType: Push Β· SetRoot Β· GoBack Β· PopToRoot Β· SwitchShell
β»οΈ ViewModel Lifecycle
| Interface | Method | Purpose |
|---|---|---|
IPageLifecycleAware |
OnAppearing() / OnDisappearing() |
Page visibility hooks |
INavigationConfirmation |
Task<bool> CanNavigate() |
Guard navigation (unsaved changes, etc.) |
INavigationAware |
OnNavigatingFrom(params) |
Mutate parameters before leaving |
IQueryAttributable |
ApplyQueryAttributes(params) |
Receive navigation parameters |
IDisposable |
Dispose() |
Cleanup when page leaves the stack |
β‘ Source Generation
| Generated File | What It Does |
|---|---|
Routes.g.cs |
Static route constants β Routes.Detail |
NavigationExtensions.g.cs |
Typed methods β NavigateToDetail(id, page) |
NavigationBuilderNavExtensions.g.cs |
Typed builder methods β AddDetail(id, page) |
NavigationBuilderExtensions.g.cs |
One-line DI β AddGeneratedMaps() |
Invalid route names produce SHINY001 compiler errors. Disable individual outputs via MSBuild properties.
β Zero Ceremony
- One base class change β
AppShell : ShinyShellβ for deterministic BindingContext assignment - PageβViewModel mapping with automatic BindingContext assignment
- Drop-in
[ShellMap]attribute replaces manual route registration
Getting Started
1. Install
dotnet add package Shiny.Maui.Shell
2. Configure MauiProgram.cs
With source generation (recommended):
builder
.UseMauiApp<App>()
.UseShinyShell(x => x.AddGeneratedMaps());
Manual registration:
builder
.UseMauiApp<App>()
.UseShinyShell(x => x
.Add<MainPage, MainViewModel>(registerRoute: false) // pages in AppShell.xaml
.Add<DetailPage, DetailViewModel>("Detail")
.Add<SettingsPage, SettingsViewModel>("Settings")
);
3. Set up AppShell
Your AppShell must inherit from ShinyShell instead of Shell:
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();
}
}
Pages defined in AppShell.xaml should use registerRoute: false.
4. Navigate
Inject INavigator into your ViewModels:
public class MyViewModel(INavigator navigator)
{
// Route-based navigation with args
await navigator.NavigateTo("Detail", args: [("ItemId", "123")]);
// 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");
// Root navigation β resets the stack
await navigator.NavigateTo<DashboardViewModel>(relativeNavigation: false);
// Go back with result
await navigator.GoBack(("Result", selectedItem));
// Go back multiple pages
await navigator.GoBack(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>();
// Set or clear a numeric badge on a tab in the active Shell
await navigator.SetTabBadge("Inbox", 3);
await navigator.SetTabBadge<InboxViewModel>(7);
await navigator.ClearTabBadge("Inbox");
await navigator.ClearTabBadge<InboxViewModel>();
// Fluent multi-segment navigation builder
await navigator
.CreateBuilder()
.AddDetail(id: 42)
.AddModal()
.Navigate();
// Pop back 2 pages, then push
await navigator
.CreateBuilder()
.PopBack(2)
.AddHome()
.Navigate();
// Navigate from root with builder
await navigator
.CreateBuilder(fromRoot: true)
.AddDashboard()
.AddDetail(id: 1)
.Navigate();
}
Root navigation (relativeNavigation: false or CreateBuilder(fromRoot: true)) uses the // URI prefix, which requires the target route to be declared in your AppShell.xaml. Routes registered only via Routing.RegisterRoute or [ShellMap] cannot be navigated to from root. Add the page as a ShellContent in your Shell XAML and use registerRoute: false in [ShellMap].
If you're setting arguments on the ViewModel navigation, you should make them observable if they are bound on the Page.
Tab badges only work for routes that are already present as tabs in the active Shell. The badge APIs are supported on Android, iOS, Mac Catalyst, and Windows. Linux and macOS AppKit throw PlatformNotSupportedException.
4.1 XAML Navigation
Use Navigate attached properties when you want route-based navigation directly from XAML without a ViewModel command:
<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">
<Button Text="Open Detail"
shiny:Navigate.Route="Detail"
shiny:Navigate.ParameterKey="ItemId"
shiny:Navigate.ParameterValue="{Binding SelectedId}" />
<ToolbarItem Text="Home"
shiny:Navigate.Route="MainPage"
shiny:Navigate.RelativeNavigation="False" />
</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>
Navigate currently supports Button, MenuItem, and ToolbarItem.
5. Dialogs
Inject IDialogs for user-facing dialogs:
public class MyViewModel(IDialogs dialogs)
{
// Alert
await dialogs.Alert("Error", "Something went wrong");
// Confirm
if (await dialogs.Confirm("Delete?", "Are you sure?"))
{
// delete
}
// Prompt for text input
var name = await dialogs.Prompt("Name", "Enter your name", placeholder: "John Doe");
if (name != null)
{
// user entered a value
}
// Action sheet
var choice = await dialogs.ActionSheet("Options", "Cancel", "Delete", "Edit", "Share");
}
6. UxDivers Dialogs (Optional)
Replace the default platform dialogs with styled popups from UXDivers Popups:
dotnet add package UXDivers.Popups.Maui
Add theme dictionaries to App.xaml:
<ResourceDictionary.MergedDictionaries>
<uxd:DarkTheme xmlns:uxd="clr-namespace:UXDivers.Popups.Maui.Controls;assembly=UXDivers.Popups.Maui" />
<uxd:PopupStyles xmlns:uxd="clr-namespace:UXDivers.Popups.Maui.Controls;assembly=UXDivers.Popups.Maui" />
</ResourceDictionary.MergedDictionaries>
Configure in MauiProgram.cs:
builder
.UseMauiApp<App>()
.UseUxDiversDialogs() // Initialize UxDivers popup infrastructure
.UseShinyShell(x => x
.UseUxDiversDialogs() // Register as IDialogs provider
.AddGeneratedMaps()
)
Your ViewModels continue using IDialogs as before β only the visual presentation changes.
Navigation Events
Subscribe to Navigating and Navigated on INavigator for cross-cutting concerns like logging or analytics:
public class NavigationLogger(
ILogger<NavigationLogger> logger,
INavigator navigator
) : IMauiInitializeService
{
public void Initialize(IServiceProvider services)
{
navigator.Navigating += (_, args) =>
logger.LogInformation("Navigating from '{From}' to '{To}' ({Type})",
args.FromUri, args.ToUri, args.NavigationType);
navigator.Navigated += (_, args) =>
logger.LogInformation("Navigated to '{To}' - ViewModel: {VM} ({Type})",
args.ToUri, args.ToViewModel?.GetType().Name, args.NavigationType);
}
}
// Register in MauiProgram.cs
builder.Services.AddSingleton<IMauiInitializeService, NavigationLogger>();
ViewModel Lifecycle
Implement these interfaces on your ViewModels as needed. Works just like Prism Library.
[ShellMap<DetailPage>("Detail")]
public partial class DetailViewModel(INavigator navigator, IDialogs dialogs) : ObservableObject,
IQueryAttributable,
IPageLifecycleAware,
INavigationConfirmation,
IDisposable
{
[ShellProperty]
[ObservableProperty]
string itemId;
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue(nameof(ItemId), out var id))
ItemId = id?.ToString();
}
public void OnAppearing() { /* load data */ }
public void OnDisappearing() { /* pause */ }
public async Task<bool> CanNavigate()
{
if (!hasUnsavedChanges) return true;
return await dialogs.Confirm("Unsaved Changes", "Discard changes?");
}
public void Dispose() { /* cleanup */ }
}
Source Generation
Decorate your ViewModels with [ShellMap] and [ShellProperty] to eliminate boilerplate:
Input:
[ShellMap<DetailPage>("Detail")]
public partial class DetailViewModel : ObservableObject
{
[ShellProperty]
public string ItemId { get; set; }
[ShellProperty(required: false)]
public int Page { get; set; }
}
Generated output:
// Routes.g.cs β constant name matches the route parameter
public static class Routes
{
public const string Detail = "Detail";
}
// NavigationExtensions.g.cs β typed INavigator methods
public static class NavigationExtensions
{
public static Task NavigateToDetail(this INavigator navigator, string itemId,
int page = default, bool relativeNavigation = true)
{
return navigator.NavigateTo<DetailViewModel>(x =>
{
x.ItemId = itemId;
x.Page = page;
}, relativeNavigation);
}
}
// NavigationBuilderNavExtensions.g.cs β typed INavigationBuilder methods
public static class NavigationBuilderNavExtensions
{
public static INavigationBuilder AddDetail(this INavigationBuilder builder,
string itemId, int page = default)
{
return builder.Add<DetailViewModel>(x => { x.ItemId = itemId; x.Page = page; });
}
}
// NavigationBuilderExtensions.g.cs β uses string literals (not Routes.*)
public static class NavigationBuilderExtensions
{
public static ShinyAppBuilder AddGeneratedMaps(this ShinyAppBuilder builder)
{
builder.Add<DetailPage, DetailViewModel>("Detail");
return builder;
}
}
Then use it:
// MauiProgram.cs - one line to register everything
builder.UseShinyShell(x => x.AddGeneratedMaps());
// Navigate with generated extension methods - no guesswork
await navigator.NavigateToDetail("123", page: 2);
// Fluent builder with generated extensions
await navigator.CreateBuilder().AddDetail("123", page: 2).Navigate();
Route Naming
The route parameter in [ShellMap] drives the generated constant and method names. It must be a valid C# identifier β invalid names produce a SHINY001 compiler error.
// Route drives the constant and method name
[ShellMap<HomePage>("Dashboard")]
// β Routes.Dashboard = "Dashboard"
// β NavigateToDashboard(...)
// No route β falls back to page type name without "Page" suffix
[ShellMap<HomePage>]
// β Routes.Home = "HomePage"
// β NavigateToHome(...)
Configuring Source Generation
Disable individual generated files via MSBuild properties:
<PropertyGroup>
<ShinyMauiShell_GenerateRouteConstants>false</ShinyMauiShell_GenerateRouteConstants>
<ShinyMauiShell_GenerateNavExtensions>false</ShinyMauiShell_GenerateNavExtensions>
</PropertyGroup>
NavigationBuilderExtensions.g.cs (AddGeneratedMaps()) is only generated when [ShellMap] attributes are detected and ShinyMauiShell_GenerateNavExtensions is not set to false. A SHINY002 warning is emitted if maps are detected but nav extensions are disabled.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-android36.0 is compatible. net10.0-browser was computed. net10.0-ios was computed. net10.0-ios26.0 is compatible. net10.0-maccatalyst was computed. net10.0-maccatalyst26.0 is compatible. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net10.0-windows10.0.19041 is compatible. |
-
net10.0
- Microsoft.Maui.Controls (>= 10.0.51)
-
net10.0-android36.0
- Microsoft.Maui.Controls (>= 10.0.51)
-
net10.0-ios26.0
- Microsoft.Maui.Controls (>= 10.0.51)
-
net10.0-maccatalyst26.0
- Microsoft.Maui.Controls (>= 10.0.51)
-
net10.0-windows10.0.19041
- Microsoft.Maui.Controls (>= 10.0.51)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Shiny.Maui.Shell:
| Package | Downloads |
|---|---|
|
Shiny.Maui.Shell.UxDiversDialogs
Shiny MAUI Shell - Make .NET MAUI shell a pleasant experience |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on Shiny.Maui.Shell:
| Repository | Stars |
|---|---|
|
shinyorg/shiny
.NET Framework for Backgrounding & Device Hardware Services (iOS, MacCatalyst, Android, & Windows)
|
| Version | Downloads | Last Updated |
|---|---|---|
| 6.0.3 | 172 | 4/29/2026 |
| 6.0.3-beta-0004 | 97 | 4/29/2026 |
| 6.0.3-beta-0003 | 106 | 4/29/2026 |
| 6.0.3-beta-0002 | 100 | 4/29/2026 |
| 6.0.2 | 104 | 4/29/2026 |
| 6.0.2-beta-0002 | 108 | 4/29/2026 |
| 6.0.1 | 105 | 4/28/2026 |
| 6.0.1-beta-0003 | 104 | 4/28/2026 |
| 6.0.1-beta-0002 | 101 | 4/27/2026 |
| 6.0.0 | 119 | 4/27/2026 |
| 6.0.0-beta-0001 | 102 | 4/26/2026 |
| 5.0.0 | 202 | 4/22/2026 |
| 5.0.0-beta-0001 | 102 | 4/21/2026 |
| 4.2.1 | 139 | 4/16/2026 |
| 4.2.0 | 105 | 4/15/2026 |
| 4.2.0-beta-0002 | 97 | 4/15/2026 |
| 4.1.0 | 182 | 4/14/2026 |
| 4.1.0-beta-0002 | 99 | 4/14/2026 |
| 4.0.0 | 109 | 4/11/2026 |
| 4.0.0-beta-0004 | 95 | 4/12/2026 |