Shiny.Maui.Shell 7.0.0

Prefix Reserved
There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Shiny.Maui.Shell" Version="7.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Shiny.Maui.Shell" Version="7.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Shiny.Maui.Shell" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Shiny.Maui.Shell --version 7.0.0
                    
#r "nuget: Shiny.Maui.Shell, 7.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Shiny.Maui.Shell@7.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Shiny.Maui.Shell&version=7.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Shiny.Maui.Shell&version=7.0.0
                    
Install as a Cake Tool

Shiny MAUI Shell

NuGet

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.

Full Documentation


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 INavigator for clean separation of concerns.

Alternative providers (same IDialogs interface, 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() or ShinyAppBuilder.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.
    }
}

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

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): INavigator calls, app links and shortcuts do not consult it. Asked before the interceptors, and unaffected by bypassInterceptors, which is about the interceptor chain only.
  • Navigating / Navigated events - 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.


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, so 1.5 parses 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.

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

  1. Describe your routes β€” Add description to [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;
}
  1. Install Microsoft.Extensions.AI β€” AI extensions are enabled by default when this package is referenced:
dotnet add package Microsoft.Extensions.AI
  1. Register AI tools in DI β€” Use the generated AddAiTools() extension:
builder.UseShinyShell(x => x
    .AddGeneratedMaps()
    .AddAiTools()          // registers AiMauiShellTools as singleton
);
  1. Inject and use AiMauiShellTools β€” The generated class provides Prompt and Tools properties:
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:

  1. User taps Login with GitHub β€” the app requests a device code and opens github.com/login/device in the browser
  2. User enters the displayed code to authorize
  3. The app exchanges the GitHub token for a Copilot API token and creates an IChatClient via Microsoft.Extensions.AI.OpenAI (the Copilot API is OpenAI-compatible)
  4. The chat injects AiMauiShellTools which provides Prompt and Tools for route discovery and navigation

The relevant sample files:

  • Sample/AI/ChatPage.xaml β€” Chat UI using Shiny.Maui.Controls.ChatView
  • Sample/AI/ChatViewModel.cs β€” AI client setup and tool registration
  • Sample/AI/GitHubCopilotAuthService.cs β€” Device flow OAuth + token management
  • Sample/AI/TestWorkOrderViewModel.cs β€” AI-navigable work order form
  • Sample/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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed