Shiny.Maui.Shell
7.0.0
Prefix Reserved
See the version list below for details.
dotnet add package Shiny.Maui.Shell --version 7.0.0
NuGet\Install-Package Shiny.Maui.Shell -Version 7.0.0
<PackageReference Include="Shiny.Maui.Shell" Version="7.0.0" />
<PackageVersion Include="Shiny.Maui.Shell" Version="7.0.0" />
<PackageReference Include="Shiny.Maui.Shell" />
paket add Shiny.Maui.Shell --version 7.0.0
#r "nuget: Shiny.Maui.Shell, 7.0.0"
#:package Shiny.Maui.Shell@7.0.0
#addin nuget:?package=Shiny.Maui.Shell&version=7.0.0
#tool nuget:?package=Shiny.Maui.Shell&version=7.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 |
π‘οΈ Navigation Interceptors β INavigationInterceptor
| Capability | Description |
|---|---|
| Guard every navigation | Routes, ViewModels, builder, back, app links, shortcuts, tab taps |
| Cancel | NavigationInterceptorResult.Cancel() β the user stays put |
| Redirect | Redirect("//Login") or refactor-safe Redirect<LoginViewModel>() |
| Destination ViewModel | Resolved and populated before the interceptor runs, so guards can read it |
| Chained | Multiple interceptors run in Order then registration order; a redirect re-runs the chain |
| Bypassable | NavigateTo(..., bypassInterceptors: true) for the navigation a guard itself performs |
| Answerable | Every navigation method returns Task<bool> - false when a guard cancelled it |
π¬ 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> |
For anything richer than the four primitives above, present one of your own pages as a dialog and
await a typed result from it with INavigator.ShowDialog<TViewModel, T> β see
ViewModel Dialogs.
Thread-safe β dispatches to UI thread automatically. Inject separately from
INavigatorfor clean separation of concerns.Alternative providers (same
IDialogsinterface, no ViewModel changes needed):
Shiny.Maui.Shell.ShinyDialogsβ owned, animated, themeable dialogs powered by Shiny.Maui.Controls (never the native alert/prompt; identical across platforms).Shiny.Maui.Shell.UxDiversDialogsβ styled popup dialogs powered by UXDivers Popups.Both packages also ship an
IDialogPresenter, so your ViewModel dialogs render as a card over a dimmed backdrop instead of a modal page β see Changing how dialogs appear.
π‘ 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 leaving the page - user-driven navigation only (tab tap, flyout, hardware back) |
INavigationAware |
OnNavigatingFrom(params) |
Mutate parameters before leaving |
IQueryAttributable |
ApplyQueryAttributes(params) |
Receive navigation parameters (only for string-based NavigateTo β not needed with [ShellProperty]) |
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) with XML docs and [Description] attributes |
NavigationBuilderNavExtensions.g.cs |
Typed builder methods β AddDetail(id, page) |
NavigationBuilderExtensions.g.cs |
One-line DI β AddGeneratedMaps() |
AiExtensions.g.cs |
Route metadata β GetGeneratedRouteInfo(), plus AiMauiShellTools class with Prompt, Tools, GetAiToolApplicableGeneratedRoutes(), and NavigateToRoute() when AI extensions are enabled. Also generates AddAiTools() extension on ShinyAppBuilder for DI registration |
Invalid route names produce SHINY001 compiler errors. Disable individual outputs via MSBuild properties.
π Custom Handlers
| Handler | Description |
|---|---|
DisableShellFlyoutSwipeHandler |
Disables the flyout swipe gesture while keeping the hamburger button functional. Opt-in via DisableShellFlyoutSwipeHandler.Register() |
β 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. Shiny Controls Dialogs (Optional)
Replace the default native dialogs with the owned, animated, themeable dialog service from Shiny.Maui.Controls. The dialog is always rendered by the library, so it looks identical across platforms:
dotnet add package Shiny.Maui.Shell.ShinyDialogs
Configure in MauiProgram.cs β UseShinyControls() registers the underlying IDialogService, and UseShinyDialogs() routes IDialogs through it:
builder
.UseMauiApp<App>()
.UseShinyControls() // registers the Shiny.Maui.Controls IDialogService
.UseShinyShell(x => x
.UseShinyDialogs() // route IDialogs through the Controls dialog service
.UseShinyDialogPresenter() // ...and render ViewModel dialogs as an overlay card
.AddGeneratedMaps()
)
Your ViewModels continue using IDialogs as before β only the visual presentation changes.
UseShinyDialogPresenter() is the matching half for ViewModel dialogs: instead
of pushing the dialog page onto Shell's modal stack, it floats the page's content as a themed card
over a dimmed backdrop, using the active Shiny theme's Surface and Scrim colours. On a
ShinyContentPage the overlay goes into that page's own OverlayHost; on a plain ContentPage it is
layered over the content.
.UseShinyDialogPresenter(o =>
{
o.BackdropOpacity = 0.6;
o.CornerRadius = 24;
o.MaxWidth = 480;
o.DismissOnBackdropTap = false; // dialog closes only via the ViewModel's events
})
7. 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>()
.UseShinyShell(x => x
.UseUxDiversDialogs() // route IDialogs through UXDivers popups
.UseUxDiversDialogPresenter() // ...and render ViewModel dialogs as a popup
.AddGeneratedMaps()
)
Either call initializes the UXDivers popup infrastructure (UseUXDiversPopups()), and doing both
initializes it once.
Your ViewModels continue using IDialogs as before β only the visual presentation changes.
UseUxDiversDialogPresenter() is the matching half for ViewModel dialogs: the
dialog page's content is hosted in a UXDivers PopupPage β a card over a dimmed backdrop, built the
way their own custom popups are, so it matches the alert/confirm/prompt popups beside it.
.UseUxDiversDialogPresenter(o =>
{
o.BackdropOpacity = 0.6;
o.CornerRadius = 24;
o.MaxWidth = 480;
o.ConfigurePopup = popup => popup.AppearingAnimation = new MoveInPopupAnimation();
})
8. ViewModel Dialogs
IDialogs covers alert / confirm / prompt / action sheet. When you need a real UI to collect a
result β a colour picker, a filter sheet, a signature pad β present one of your own Page/ViewModel
pairs as a dialog and await the value it produces.
The ViewModel implements IDialogAware<T> and raises one of two events to close itself:
[ShellMap<PickColorPage>("PickColor")]
public partial class PickColorViewModel : ObservableObject, IDialogAware<string>
{
public event EventHandler<string>? Completed;
public event EventHandler? Cancelled;
[ShellProperty("The colour to pre-select", required: false)]
public string Preset { get; set; } = "Red";
[RelayCommand] void Pick(string colour) => this.Completed?.Invoke(this, colour);
[RelayCommand] void Cancel() => this.Cancelled?.Invoke(this, EventArgs.Empty);
}
The source generator emits a typed Show{Route}Dialog extension for it, so the call site needs no
type arguments and [ShellProperty] values become parameters:
var result = await navigator.ShowPickColorDialog(preset: "Violet");
if (result.TryGetValue(out var colour))
this.Selected = colour;
// or
this.Selected = result.ValueOr("Red");
DialogResult<T> exists because default(T) cannot express cancellation for value types β a bool
dialog could not otherwise tell "the user chose No" apart from "the user dismissed it".
| Outcome | Result |
|---|---|
ViewModel raised Completed |
IsCancelled == false, Value set |
ViewModel raised Cancelled |
IsCancelled == true |
| User dismissed the dialog (back button, iOS swipe-down) | IsCancelled == true |
The CancellationToken you passed fired |
throws OperationCanceledException |
Every dismissal path completes the await β a dialog closed without either event being raised
reports cancellation rather than hanging.
The underlying method is available directly if you'd rather not use the generated wrapper, though it needs both type arguments spelled out (C# cannot infer a type argument from a constraint):
var result = await navigator.ShowDialog<PickColorViewModel, string>(x => x.Preset = "Violet");
The dialog ViewModel must be mapped to a page like any other navigable ViewModel β via [ShellMap]
AddGeneratedMaps()orShinyAppBuilder.Add<TPage, TViewModel>().
Lifecycle
| Hook | Dialog ViewModel | Page underneath |
|---|---|---|
IPageLifecycleAware.OnAppearing |
β when shown | β when the dialog closes |
IPageLifecycleAware.OnDisappearing |
β when closed | β when the dialog opens |
IDisposable.Dispose |
β when closed | β |
INavigationAware.OnNavigatingFrom |
β not raised | β not raised |
INavigationConfirmation.CanNavigate |
β not consulted | β not consulted |
INavigator.Navigating / Navigated |
β not raised | β not raised |
The three that don't fire are deliberate: showing a dialog is not a navigation stack mutation, and an "are you sure you want to leave?" guard firing because a dialog opened would be wrong.
The dialog ViewModel is disposed as the page detaches, which happens marginally before ShowDialog
returns. The DialogResult<T> you get back is captured at the moment the ViewModel raised its event
and is unaffected β but don't hold on to the ViewModel instance after the await.
Changing how dialogs appear
How a dialog appears is decided by the registered IDialogPresenter β the ViewModel, the
IDialogAware<T> contract, and the call site are identical whichever one you pick.
| Presenter | Package | Presentation |
|---|---|---|
ShellModalDialogPresenter (default) |
Shiny.Maui.Shell |
The page on Shell's modal stack |
ShinyOverlayDialogPresenter |
Shiny.Maui.Shell.ShinyDialogs |
A themed card over a dimmed backdrop, in the current page |
UxDiversDialogPresenter |
Shiny.Maui.Shell.UxDiversDialogs |
A UXDivers PopupPage over a dimmed backdrop |
builder.UseShinyShell(x => x
.AddGeneratedMaps()
.UseShinyDialogPresenter() // or .UseUxDiversDialogPresenter()
);
The two overlay presenters keep the page underneath on screen behind the scrim, which changes the
lifecycle table above in one place: the page underneath does not disappear when the dialog opens,
so it neither raises OnDisappearing nor OnAppearing. The dialog ViewModel's own hooks β including
Dispose β fire exactly as they do for a modal.
Both are dismissed by a tap on the backdrop (DismissOnBackdropTap = false turns that off), and both
report a dismissal as IsCancelled. The overlay presenter also treats the host page disappearing as a
dismissal: an overlay lives inside a page, so a navigation away takes the dialog with it, and the
awaiting caller has to be released rather than left hanging.
Writing your own. Implement IDialogPresenter for anything that can host a Page:
public class MyPresenter : IDialogPresenter
{
// Show the page; complete the Task once it is gone, either because the user
// dismissed it or because `dismiss` fired. Never throw on `dismiss`.
public async Task Present(Page page, object viewModel, CancellationToken dismiss) { /* ... */ }
}
builder.UseShinyShell(x => x
.AddGeneratedMaps()
.UseDialogPresenter<MyPresenter>()
);
For a host that takes a View rather than a Page β a popup, a bottom sheet, a custom overlay β
derive from ViewDialogPresenter instead. It hands you the page's content with the binding context
already set, and takes care of what the page would otherwise have done for you: raising
IPageLifecycleAware, disposing the ViewModel, and giving the content back to its page afterwards.
public class MySheetPresenter(IMainThread mainThread) : ViewDialogPresenter(mainThread)
{
protected override async Task PresentView(View content, object viewModel, CancellationToken dismiss)
{
// Called on the main thread. Show `content`; complete once it is gone, and
// detach it from your host before returning.
}
}
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>();
Navigation Interceptors
INavigationInterceptor sits in front of every navigation the app makes and can let it through,
cancel it, or send it somewhere else. Register as many as you like - they run in registration order
and the first one to cancel or redirect wins.
public interface INavigationInterceptor
{
Task<NavigationInterceptorResult> InterceptNavigationAsync(
string uri,
object? viewModel,
CancellationToken cancellationToken
);
// Lowest runs first; ties keep registration order
int Order => 0;
}
Every navigation method returns Task<bool> - false means a guard cancelled it:
if (!await navigator.NavigateTo<DetailViewModel>())
// an interceptor said no
public class AuthNavigationInterceptor(IAuthService auth) : INavigationInterceptor
{
// Guards run before anything that only observes
public int Order => -100;
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>(); // or Redirect("//Login")
}
}
// MauiProgram.cs
builder.UseShinyShell(x => x
.AddGeneratedMaps()
.AddNavigationInterceptor<AuthNavigationInterceptor>()
.AddNavigationInterceptor<AuditNavigationInterceptor>()
// or inline, for a one-line rule
.AddNavigationInterceptor((uri, vm, ct) =>
{
Console.WriteLine($"Navigating to {uri}");
return Task.FromResult(NavigationInterceptorResult.Continue);
}, order: 100)
);
Interceptors run in Order (lowest first), then registration order. They are singletons, so keep
no per-navigation state in fields.
What gets intercepted
| Path | Intercepted | viewModel argument |
|---|---|---|
NavigateTo(route) |
β | Resolved from the route's ViewModel mapping |
NavigateTo<TViewModel>(configure) |
β | Your instance, after configure ran |
CreateBuilder()...Navigate() |
β | The last segment's ViewModel (the page the user lands on) |
GoBack / PopToRoot |
β | The existing ViewModel from the navigation stack |
| App links & app shortcuts | β | The ViewModel with the link's values already applied |
| Tab taps, flyout items, hardware back | β | null - Shell builds these, so Shiny doesn't construct one |
ShowDialog / SwitchShell |
β | Not navigation |
The ViewModel handed over is the destination one, resolved and fully populated before the
interceptor runs - so a guard can decide on the destination's own state, not just its URI, and any
change it makes sticks: that instance is what gets bound to the page. (Navigation arguments are
applied by Shell afterwards, so they win over an interceptor's edits to the same property, and a
route declared as a ShellContent in AppShell XAML keeps the ViewModel its page was already bound
to - the interceptor sees a resolved instance, but changes to it do not reach an on-screen page.)
The page being left comes from INavigationContextAccessor, along with the rest of the
navigation:
public class UnsavedChangesNavigationInterceptor(
INavigationContextAccessor context,
IDialogs dialogs
) : INavigationInterceptor
{
public async Task<NavigationInterceptorResult> InterceptNavigationAsync(string uri, object? viewModel)
{
if (context.Current?.FromViewModel is not IUnsavedChanges { HasUnsavedChanges: true })
return NavigationInterceptorResult.Continue;
return await dialogs.Confirm("Unsaved Changes", "Discard changes?")
? NavigationInterceptorResult.Continue
: NavigationInterceptorResult.Cancel();
}
}
NavigationContext carries FromUri, FromViewModel, ToUri, NavigationType, Direction,
Parameters and RedirectCount. It is only set while an interceptor is running.
Direction is the coarse question NavigationType answers precisely - Forward (push), Back
(GoBack, PopToRoot) or Root (absolute route, Shell swap). It is on NavigationEventArgs and
NavigatedEventArgs too, and any NavigationType converts with .GetDirection().
The escape hatch
A guard that navigates would otherwise guard itself. Every navigation method takes
bypassInterceptors:
await navigator.NavigateTo<LoginViewModel>(bypassInterceptors: true);
await navigator.GoBack(1, bypassInterceptors: true);
await navigator.PopToRoot(bypassInterceptors: true);
await navigator.CreateBuilder().AddDetail(42).Navigate(bypassInterceptors: true);
// the builder is fluent, so it reads fluently too
await navigator.CreateBuilder().BypassInterceptors().AddDetail(42).Navigate();
A RedirectUri never needs it - the chain restarting on a redirect is deliberate, and a redirect
to the destination already being navigated to is ignored rather than looping.
Interceptors also receive the CancellationToken passed to the navigation call - for the network
call an auth guard makes, not for the decision itself. Cancelling it abandons the navigation with
an OperationCanceledException.
Cancel and redirect
| Result | Behaviour |
|---|---|
NavigationInterceptorResult.Continue |
Next interceptor, then navigate |
Cancel() |
Nothing navigates, the rest of the chain is skipped, the caller's Task completes normally |
Redirect("Detail") |
Pushes |
Redirect("//Main/Home") |
Resets the Shell stack |
Redirect("/Login") |
Same as //Login - a single leading slash is promoted |
Redirect<LoginViewModel>() |
Resets the stack to that ViewModel's route (refactor-safe) |
Redirect<DetailViewModel>(relativeNavigation: true) |
Pushes that ViewModel's route |
A redirect restarts the whole chain against the new URI, so the redirect target is guarded as thoroughly as the original destination - and the abandoned destination's ViewModel is dropped rather than bound to anything. Redirecting to the URI already being navigated to is ignored (an unconditional "go to login" guard says this every time the user navigates to login); a genuine redirect loop throws after 10 hops rather than hanging.
An exception thrown from an interceptor propagates to the caller and the navigation does not happen - a guard that fails is never treated as a guard that passed. On tab taps and hardware back, where there is no caller, the exception is logged and the navigation is cancelled.
A blocked app link reports AppLinkResult.Blocked from IAppLinks.Handle - distinct from
Unhandled (nothing matched), because the platform hooks still report a blocked link as handled:
saying otherwise invites iOS to open the URL in a browser instead, which is the opposite of what a
guard that just blocked it wants.
Asking the user
The interceptor is async, so a dialog is a legal thing to await - the navigation has not been
handed to Shell yet and simply waits on the answer. One action sheet can produce all three
outcomes (this is AskFirstNavigationInterceptor, on the sample's Route Interceptor page):
public class AskFirstNavigationInterceptor(
IDialogs dialogs,
INavigationContextAccessor context
) : INavigationInterceptor
{
const string LetItGo = "Let it through";
const string SendElsewhere = "Redirect to Lifecycle";
const string StopIt = "Stop navigation";
public async Task<NavigationInterceptorResult> InterceptNavigationAsync(
string uri,
object? viewModel,
CancellationToken cancellationToken
)
{
// narrow to one destination - otherwise every tab tap and back press prompts
if (viewModel is not DetailViewModel detail)
return NavigationInterceptorResult.Continue;
var choice = await dialogs.ActionSheet(
$"{context.Current?.FromUri} -> {uri} (Text: '{detail.Text}')",
cancel: StopIt, // dismissing the sheet lands here too
destruction: null,
buttons: [LetItGo, SendElsewhere]
);
return choice switch
{
LetItGo => NavigationInterceptorResult.Continue,
SendElsewhere => NavigationInterceptorResult.Redirect<LifecycleDemoViewModel>(relativeNavigation: true),
_ => NavigationInterceptorResult.Cancel()
};
}
}
Two things worth copying from it: the early Continue when the destination is not the one being
guarded (an interceptor sees every navigation, so an unguarded prompt fires on tab taps), and
treating the sheet's cancel/dismiss path as Cancel() - the safe default when the user did not
actually answer.
Interceptors vs. the other hooks
INavigationInterceptor- app-wide, about the destination, can cancel and redirect.INavigationConfirmation- implemented by the ViewModel being left, answers "may I leave?", and only applies to user-driven Shell navigation (tab tap, flyout item, hardware back button):INavigatorcalls, app links and shortcuts do not consult it. Asked before the interceptors, and unaffected bybypassInterceptors, which is about the interceptor chain only.Navigating/Navigatedevents - observation only, they cannot change the outcome.
ViewModel Lifecycle
Implement these interfaces on your ViewModels as needed. Works just like Prism Library.
[ShellMap<DetailPage>("Detail", description: "Navigate to the detail page")]
public partial class DetailViewModel(INavigator navigator, IDialogs dialogs) : ObservableObject,
IPageLifecycleAware,
INavigationConfirmation,
IDisposable
{
[ShellProperty("The item identifier")]
[ObservableProperty]
string itemId;
public void OnAppearing() { /* load data */ }
public void OnDisappearing() { /* pause */ }
// Asked when the user navigates away themselves - a tab tap, a flyout item, the hardware
// back button. Programmatic navigation does not consult it; guard that with an
// INavigationInterceptor (see Navigation Interceptors above).
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", description: "Navigate to the detail page")]
public partial class DetailViewModel : ObservableObject
{
[ShellProperty("The item identifier")]
public string ItemId { get; set; }
[ShellProperty("Page number for pagination", 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 with XML docs and [Description] attributes
public static class NavigationExtensions
{
/// <summary>
/// Navigate to the detail page
/// </summary>
/// <param name="itemId">The item identifier</param>
/// <param name="page">Page number for pagination</param>
/// <param name="relativeNavigation">If true, it will navigate/stack from where the application currently is otherwise, it will reset the stack to this new route</param>
[Description("Navigate to the detail page")]
public static Task NavigateToDetail(this INavigator navigator,
[Description("The item identifier")] string itemId,
[Description("Page number for pagination")] int page = default,
[Description("If true, it will navigate/stack from where the application currently is otherwise, it will reset the stack to this new route")] 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;
}
}
// DialogExtensions.g.cs β only for ViewModels that also implement IDialogAware<T>
public static class DialogExtensions
{
/// <summary>Navigate to the detail page</summary>
/// <param name="itemId">The item identifier</param>
/// <param name="cancellationToken">Dismisses the dialog and throws OperationCanceledException. Distinct from the user cancelling, which returns a cancelled DialogResult.</param>
[Description("Navigate to the detail page")]
public static Task<DialogResult<string>> ShowDetailDialog(this INavigator navigator,
[Description("The item identifier")] string itemId,
[Description("Page number for pagination")] int page = default,
CancellationToken cancellationToken = default)
{
return navigator.ShowDialog<DetailViewModel, string>(x =>
{
x.ItemId = itemId;
x.Page = page;
}, cancellationToken);
}
}
// AiExtensions.g.cs β route metadata (always generated)
public static class AiExtensions
{
[Description("This provides a list of routes throughout the application")]
public static GeneratedRouteInfo[] GetGeneratedRouteInfo(this INavigator navigator) =>
[
new("Detail", "Navigate to the detail page",
[new("ItemId", "The item identifier", "string", true),
new("Page", "Page number for pagination", "int", false)])
];
}
// --- AI class and DI extension below generated when AI extensions are enabled ---
// (enabled by default when Microsoft.Extensions.AI is referenced)
// AiMauiShellTools β inject via DI for AI-powered navigation
public class AiMauiShellTools
{
public AiMauiShellTools(INavigator navigator) { ... }
// Pre-formatted prompt describing all AI-applicable routes
public string Prompt { get; }
// Ready-to-use AITool[] for route discovery and navigation
public AITool[] Tools { get; }
// Filtered routes with descriptions and parameters
public GeneratedRouteInfo[] GetAiToolApplicableGeneratedRoutes() => ...;
// AI-friendly navigation with automatic type conversion
public Task<string> NavigateToRoute(string route, Dictionary<string, string>? args = null) { ... }
}
// DI registration extension
public static class AiMauiShellToolsExtensions
{
public static ShinyAppBuilder AddAiTools(this ShinyAppBuilder builder) { ... }
}
Then use it:
// MauiProgram.cs - one line to register everything, including AI tools
builder.UseShinyShell(x => x
.AddGeneratedMaps()
.AddAiTools() // registers AiMauiShellTools as singleton
);
// Navigate with generated extension methods - no guesswork
await navigator.NavigateToDetail("123", page: 2);
// Present a dialog-aware ViewModel and await its typed result - no type arguments needed
var result = await navigator.ShowPickColorDialog(preset: "Violet");
// Fluent builder with generated extensions
await navigator.CreateBuilder().AddDetail("123", page: 2).Navigate();
// Get route metadata for tooling
var routes = navigator.GetGeneratedRouteInfo();
// AI integration β inject AiMauiShellTools via DI
public class ChatViewModel(AiMauiShellTools aiTools)
{
var options = new ChatOptions { Tools = [.. aiTools.Tools] };
// aiTools.Prompt contains the pre-formatted route prompt
}
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>
<ShinyMauiShell_GenerateAiExtensions>false</ShinyMauiShell_GenerateAiExtensions>
<ShinyMauiShell_AiToolsClassName>MyAppAiTools</ShinyMauiShell_AiToolsClassName>
<ShinyMauiShell_AiExtensionsClassName>MyAppRouteExtensions</ShinyMauiShell_AiExtensionsClassName>
<ShinyMauiShell_AiNavigateMethodName>GoToPage</ShinyMauiShell_AiNavigateMethodName>
</PropertyGroup>
| Property | Default | Controls |
|---|---|---|
ShinyMauiShell_GenerateRouteConstants |
true |
Routes.g.cs |
ShinyMauiShell_GenerateNavExtensions |
true |
All navigation extensions, DialogExtensions.g.cs, and AddGeneratedMaps |
ShinyMauiShell_GenerateAiExtensions |
true |
AiMauiShellTools class, AddAiTools(), GetAiToolApplicableGeneratedRoutes, NavigateToRoute, and Prompt. Requires Microsoft.Extensions.AI package (SHINY003 error if missing). Set to false to disable |
ShinyMauiShell_AiToolsClassName |
AiMauiShellTools |
Class name for the generated AI tools class |
ShinyMauiShell_AiExtensionsClassName |
AiExtensions |
Class name for the static route info extensions class |
ShinyMauiShell_AiNavigateMethodName |
NavigateToRoute |
Method name for the AI-friendly navigate method |
ShinyAppLinkSchemes |
(none) | Semicolon-separated custom URL schemes for App Links |
ShinyAppLinkDomains |
(none) | Semicolon-separated universal/app link domains |
ShinyAppLinkValidation |
true |
Set to false to silence the manifest validation warnings |
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.
DialogExtensions.g.cs is only generated when at least one [ShellMap] ViewModel also implements IDialogAware<T>. Dialog methods are deliberately excluded from the AI tool surface β an AI agent should be driving navigation, not blocking on a modal awaiting human input.
App Links
Deep links are declared where the route already is β the appLinks argument of [ShellMap]:
[ShellMap<ProductPage>(
description: "Shows a product",
appLinks: ["product/{id}", "p/{id}"]
)]
public partial class ProductViewModel : ObservableObject
{
[ShellProperty("The product id")] public int Id { get; set; }
[ShellProperty(required: false)] public string? Tab { get; set; }
}
myapp://product/123?tab=reviews and https://shinylib.net/p/123 both open ProductPage with
Id = 123 and Tab = "reviews".
{token}path segments bind to the[ShellProperty]of the same name (case-insensitive).- Query string values bind by property name too. A path token wins over a query value of the same name.
- Any configured scheme or domain serves any template β adding a domain later needs no attribute change.
- Values are converted with
InvariantCulture, so1.5parses the same on every device. - A missing or unparseable required value is a routing miss, not a crash: the next-best template
is tried, then
OnUnhandled.
Setup
<PropertyGroup>
<ShinyAppLinkSchemes>myapp</ShinyAppLinkSchemes>
<ShinyAppLinkDomains>shinylib.net;www.shinylib.net</ShinyAppLinkDomains>
</PropertyGroup>
.UseShinyShell(x => x.AddGeneratedMaps())
That is the whole setup. Declaring a template is the opt-in β AddGeneratedMaps() installs the
platform delivery points itself (iOS OpenUrl and ContinueUserActivity, Android OnCreate and
OnNewIntent), so your AppDelegate, MainActivity and App classes stay untouched. Windows has
no automatic hook; forward activation to IAppLinks.Handle(uri), which returns an
AppLinkResult β Navigated, Blocked (a navigation interceptor
cancelled it) or Unhandled (nothing matched). Treat anything other than Unhandled as handled.
Both the AppDelegate and UISceneDelegate variants are hooked, because MauiUISceneDelegate
raises only the Scene-prefixed events and does not forward to the others β so an app that declares
UIApplicationSceneManifest would otherwise get dead links. iOS calls one delegate or the other,
never both.
UseAppLinks(...) is optional and only changes defaults:
Push or reset is inferred, not configured
registerRoute |
What the route is | An app link |
|---|---|---|
false |
ShellContent / tab / flyout item in AppShell XAML |
resets the stack β //route |
true |
Routing.RegisterRoute'd detail page |
pushes onto the current stack |
You already said which it is; the library does not ask twice. On a cold start a pushed route lands
on Shell's default item, or on AppLinkOptions.DefaultRoot when you set one.
.UseShinyShell(x => x
.AddGeneratedMaps()
.UseAppLinks(o =>
{
o.DefaultRoot = "//main/home"; // back stack for cold-start pushes
o.ResolveRoute = match => "//somewhere"; // last word on the destination
o.OnUnhandled = uri => Task.FromResult(false);
})
)
Manifests
The build validates your manifests and emits a warning containing the exact markup to paste. It
does not edit them β Android's merged manifest names the launcher activity with a CRC64 hash of its
namespace that MSBuild cannot compute, and Apple universal links additionally need an
apple-app-site-association file on the domain plus the Associated Domains capability on the App ID.
| Code | Platform | Missing |
|---|---|---|
| SHINY101 | Android | [IntentFilter] for a custom scheme |
| SHINY102 | Android | Verified (AutoVerify) [IntentFilter] for a domain |
| SHINY103 | iOS / MacCatalyst | CFBundleURLTypes in Info.plist |
| SHINY104 | iOS / MacCatalyst | com.apple.developer.associated-domains in Entitlements.plist |
| SHINY105 | Windows | windows.protocol extension in Package.appxmanifest |
Set <ShinyAppLinkValidation>false</ShinyAppLinkValidation> to silence them. Creating
Platforms/iOS/Entitlements.plist is enough on its own β the SDK picks it up without a
CodesignEntitlements property.
Building links to share
When exactly one scheme (or, failing that, one domain) is configured, an outbound builder is generated per route:
var uri = navigator.CreateProductAppLink(id: 42, tab: "reviews");
// myapp://product/42?Tab=reviews
Diagnostics
| Code | Severity | Meaning |
|---|---|---|
| SHINY005 | Error | Template token has no matching [ShellProperty] |
| SHINY006 | Error | A templated property's type cannot be converted from a URL string |
| SHINY007 | Error | Two routes declare templates of the same shape |
| SHINY008 | Warning | appLinks declared but no scheme or domain configured |
| SHINY009 | Warning | A required property is not a token in the template, so links must supply it as a query value |
App Shortcuts
Home screen quick actions (iOS long-press, Android app shortcuts), declared on the route they open:
[ShellMap<SearchPage>(
Shortcut = "Search",
ShortcutSubtitle = "Find anything",
ShortcutIcon = "search",
ShortcutOrder = 0
)]
public partial class SearchViewModel : ObservableObject { }
.UseShinyShell(x => x.AddGeneratedMaps())
Setting Shortcut is what declares the quick action; the other three are optional refinements.
The route becomes the shortcut's id, so there is no magic string to keep in sync and no
hand-written switch over activations.
Platform delivery is MAUI's AppActions β no AppDelegate, MainActivity or manifest work.
Whether an activation pushes or resets the stack is inferred from registerRoute, exactly as it is
for App Links.
Routes that need values
An attribute cannot supply a runtime value, so a route with a required [ShellProperty] cannot
declare a shortcut this way β that is a SHINY010 error. Register those by hand instead:
.UseShinyShell(x => x
.AddGeneratedMaps()
.AddAppShortcut<ProductViewModel>(
"Featured",
icon: "star",
id: "featured-product",
configure: vm => vm.Id = 42
)
)
The lambda works even though shortcuts outlive the process: only the id is persisted by the platform, and the registration is rebuilt every launch and looked up by id on activation.
AddAppShortcut<TViewModel> is also the way to use shortcuts with source generation turned off β
AddGeneratedMaps() simply emits calls to it.
Localized titles
The declared strings are attribute literals, so they cannot be translated on their own. Register an
IAppShortcutText to resolve them at install time, when CurrentUICulture is known:
public class ResourceShortcutText : IAppShortcutText
{
public string GetTitle(string route, string declared) => AppResources.ResourceManager.GetString(declared) ?? declared;
public string? GetSubtitle(string route, string? declared) => declared is null ? null : AppResources.ResourceManager.GetString(declared) ?? declared;
}
.UseShinyShell(x => x
.AddGeneratedMaps()
.UseShortcutText<ResourceShortcutText>()
)
The declared string becomes the resource key, with the literal as its own fallback. It applies to generated and hand-registered shortcuts alike.
Installed shortcuts keep their text until pushed again, so after a language change call:
await appShortcuts.Refresh(); // IAppShortcuts
Diagnostics
| Code | Severity | Meaning |
|---|---|---|
| SHINY010 | Error | Shortcut set on a route with a required [ShellProperty] β use AddAppShortcut(configure:) |
| SHINY011 | Warning | More than four shortcuts β iOS silently drops the excess |
| SHINY012 | Error | A Shortcut* property set without Shortcut (the title), so nothing is declared |
AddAppShortcut logs the SHINY011 equivalent at runtime, since hand-registered shortcuts get no
compile-time check.
AI Integration
Shiny MAUI Shell's source generation produces metadata and navigation methods designed for AI tool calling via Microsoft.Extensions.AI. An AI chat client can discover your app's routes, understand their purpose, extract parameters from natural language, and navigate to the correct page β all with just two tools.
How It Works
- Describe your routes β Add
descriptionto[ShellMap]and[ShellProperty]to explain what each page does and what its parameters mean:
public enum WorkOrderPriority { Low, Medium, High, Urgent }
[ShellMap<WorkOrderPage>(description: "Use when the user reports something broken, malfunctioning, or needing repair")]
public partial class WorkOrderViewModel : ObservableObject
{
[ShellProperty("Summarize what is broken based on what the user said", required: true)]
public string Description { get; set; } = string.Empty;
[ShellProperty("Infer urgency from tone. Must be: Low, Medium, High, or Urgent", required: true)]
public WorkOrderPriority Priority { get; set; } = WorkOrderPriority.Medium;
}
- Install
Microsoft.Extensions.AIβ AI extensions are enabled by default when this package is referenced:
dotnet add package Microsoft.Extensions.AI
- Register AI tools in DI β Use the generated
AddAiTools()extension:
builder.UseShinyShell(x => x
.AddGeneratedMaps()
.AddAiTools() // registers AiMauiShellTools as singleton
);
- Inject and use
AiMauiShellToolsβ The generated class providesPromptandToolsproperties:
public class ChatViewModel(AiMauiShellTools aiTools)
{
// Seed the system prompt with route info
history.Add(new ChatMessage(ChatRole.System, aiTools.Prompt));
// Use ready-to-use AITool instances
var options = new ChatOptions { Tools = [.. aiTools.Tools] };
var response = await chatClient.GetResponseAsync(history, options);
}
The AI calls GetAiToolApplicableGeneratedRoutes to discover what pages exist and what they do, then calls NavigateToRoute with the appropriate route and parameters extracted from the user's message. NavigateToRoute dispatches to NavigateTo<TViewModel> with direct property setters β no string-based Shell navigation involved. String values from the AI are automatically converted to the target property type (int, bool, double, enums, DateTime, etc.).
GeneratedRouteParameter
Each parameter in the route info includes:
| Field | Description |
|---|---|
ParameterName |
The property name (used as key in NavigateToRoute args) |
Description |
From [ShellProperty("...")] β tells the AI what this field means |
TypeName |
CLR type (string, int, bool, etc.) β tells the AI what format to use |
IsRequired |
Whether the AI must provide this value |
Sample App β GitHub Copilot Authentication
The sample application includes a working AI chat demo that authenticates via GitHub Copilot using the OAuth device flow. This lets anyone with a Copilot subscription test AI-driven navigation using their own account β no API keys to configure.
The flow:
- User taps Login with GitHub β the app requests a device code and opens
github.com/login/devicein the browser - User enters the displayed code to authorize
- The app exchanges the GitHub token for a Copilot API token and creates an
IChatClientviaMicrosoft.Extensions.AI.OpenAI(the Copilot API is OpenAI-compatible) - The chat injects
AiMauiShellToolswhich providesPromptandToolsfor route discovery and navigation
The relevant sample files:
Sample/AI/ChatPage.xamlβ Chat UI usingShiny.Maui.Controls.ChatViewSample/AI/ChatViewModel.csβ AI client setup and tool registrationSample/AI/GitHubCopilotAuthService.csβ Device flow OAuth + token managementSample/AI/TestWorkOrderViewModel.csβ AI-navigable work order formSample/AI/ContactFormViewModel.csβ AI-navigable contact form
Custom Handlers
Optional handlers that are not registered by default. Call Register() in your MauiProgram.cs to opt in.
Disable Flyout Swipe
Prevents the Shell flyout from opening via swipe gesture while keeping the hamburger button functional:
using Shiny.Handlers;
// In MauiProgram.cs, before builder.Build()
DisableShellFlyoutSwipeHandler.Register();
| Platform | Behavior |
|---|---|
| Android | Locks the DrawerLayout to LockModeLockedClosed |
| iOS / Mac Catalyst | Disables UIPanGestureRecognizer on the Shell view hierarchy |
| Windows | No-op (Windows Shell has no swipe flyout) |
| 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.100)
-
net10.0-android36.0
- Microsoft.Maui.Controls (>= 10.0.100)
-
net10.0-ios26.0
- Microsoft.Maui.Controls (>= 10.0.100)
-
net10.0-maccatalyst26.0
- Microsoft.Maui.Controls (>= 10.0.100)
-
net10.0-windows10.0.19041
- Microsoft.Maui.Controls (>= 10.0.100)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Shiny.Maui.Shell:
| Package | Downloads |
|---|---|
|
Shiny.Maui.Shell.UxDiversDialogs
Shiny MAUI Shell - Make .NET MAUI shell a pleasant experience |
|
|
Shiny.Maui.Shell.ShinyDialogs
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, MacOS, tvOS, Android, Windows, Linxux, & Web)
|
| Version | Downloads | Last Updated |
|---|---|---|
| 7.0.1 | 39 | 9/11/2026 |
| 7.0.0 | 84 | 9/10/2026 |
| 7.0.0-beta-0007 | 103 | 9/6/2026 |
| 7.0.0-beta-0006 | 113 | 9/4/2026 |
| 7.0.0-beta-0002 | 107 | 9/3/2026 |
| 6.4.1 | 115 | 9/4/2026 |
| 6.4.0 | 105 | 9/3/2026 |
| 6.3.2 | 597 | 7/7/2026 |
| 6.3.1 | 231 | 6/25/2026 |
| 6.3.0 | 200 | 6/21/2026 |
| 6.2.0 | 276 | 6/4/2026 |
| 6.2.0-beta-0003 | 141 | 6/4/2026 |
| 6.2.0-beta-0002 | 123 | 6/4/2026 |
| 6.1.2 | 133 | 6/4/2026 |
| 6.1.1 | 223 | 5/27/2026 |
| 6.1.0 | 158 | 5/20/2026 |
| 6.1.0-beta-0002 | 132 | 5/20/2026 |
| 6.0.3 | 257 | 4/29/2026 |
| 6.0.3-beta-0005 | 118 | 5/19/2026 |
| 6.0.3-beta-0004 | 114 | 4/29/2026 |