| name | shiny-maui-hosting |
| description | Generate and configure Shiny MAUI Hosting for .NET - modular MAUI app configuration with IMauiModule, static ShinyHost.Services access, IAppSupport (device info + orientation/culture/timezone change events + programmatic orientation lock), IAppStore (cross-platform store version lookups and deep links for the Apple/Mac App Store, Google Play, Microsoft Store and Linux Flatpak/Snap), and IStartupService (install the app into the desktop OS launch-at-login list on Windows, macOS, and Linux). Covers the dotnet/maui-labs desktop backends - macOS AppKit (net10.0-macos) and Linux GTK4 via the companion Shiny.Extensions.MauiHosting.Linux package |
| auto_invoke | true |
| triggers | ["IMauiModule","Shiny.Extensions.MauiHosting","ShinyHost.Services","IAppSupport","IAppStore","AppStoreOptions","AppStoreResult","AddAppSupport","AddAppStore","AddInfrastructureModules","OrientationChanged","CultureChanged","TimeZoneChanged","SetOrientation","ResetOrientation","IStartupService","AddStartupService","StartupServiceOptions","StartupServiceState","run at startup","launch at login","login item","autostart","AppKit","net10.0-macos","UseMauiAppMacOS","AddMacOSEssentials","Shiny.Extensions.MauiHosting.Linux","AddLinuxAppSupport","AddLinuxAppStore","UseMauiAppLinuxGtk4","LinuxAppId","[Truncated]"] |
Shiny MAUI Hosting Skill
You are an expert in Shiny Extensions MAUI Hosting, a .NET library providing modular MAUI app configuration via IMauiModule, a static service provider accessor, an IAppSupport service for device info and orientation/culture/timezone change detection, an IAppStore service for cross-platform store info and deep links, and an IStartupService for desktop launch-at-login registration.
Platform lifecycle hooks (IIosLifecycle.*, IAndroidLifecycle.*, IMacLifecycle.*) are wired automatically by UseShiny() from Shiny.Hosting.Maui — they are not handled by this library.
When to Use This Skill
Invoke this skill when the user wants to:
- Create MAUI hosting modules with
IMauiModule
- Access the service provider via
ShinyHost.Services
- React to orientation, culture, or time-zone changes via
IAppSupport
- Programmatically lock or reset device orientation
- Check store version / deep-link to store / launch a review page via
IAppStore
- Install or remove the app from the desktop OS startup (launch at login) list via
IStartupService
Library Overview
Documentation: https://shinylib.net/mauihost/
Repository: https://github.com/shinyorg/extensions
Package: Shiny.Extensions.MauiHosting
Namespace: Shiny
Registration
Starting in v4, each capability ships as its own extension method. AddInfrastructureModules only wires modules — opt into the rest:
using Shiny;
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.AddInfrastructureModules(new MyModule(), new AnotherModule())
.AddAppSupport()
.AddStartupService()
.AddAppStore(opts =>
{
opts.AppleAppId = "1234567890";
opts.WindowsProductId = "9NBLGGH4NNS1";
opts.CountryCode = "us";
});
return builder.Build();
Each extension is idempotent (uses TryAddSingleton / HasImplementation guards) so it's safe to call from libraries.
AddAppStore has a convenience overload:
builder.AddAppStore(appleAppId: "1234567890", windowsProductId: "9NBLGGH4NNS1");
IMauiModule Interface
public interface IMauiModule
{
void Add(MauiAppBuilder builder);
void Use(IPlatformApplication app);
}
Each module implements two methods:
Add(MauiAppBuilder builder) — register services, configure the builder. Runs before the app is built.
Use(IPlatformApplication app) — post-build initialization. ShinyHost.Services is available here. Do NOT block — runs on the main thread.
public class AnalyticsModule : IMauiModule
{
public void Add(MauiAppBuilder builder)
{
builder.Services.AddSingleton<IAnalytics, AppCenterAnalytics>();
}
public void Use(IPlatformApplication app)
{
var analytics = ShinyHost.Services.GetRequiredService<IAnalytics>();
analytics.TrackEvent("AppStarted");
}
}
Static ShinyHost Access
After initialization, ShinyHost.Services provides access to the service provider from anywhere:
var service = ShinyHost.Services.GetRequiredService<IMyService>();
:::caution
ShinyHost.Services throws InvalidOperationException if accessed before initialization.
:::
IAppSupport
IAppSupport exposes device info, browser/map launch, programmatic orientation lock, and change-detection events for orientation, culture, and time zone.
public interface IAppSupport
{
Version AppVersion { get; }
string DeviceManufacturer { get; }
string DeviceModel { get; }
Version? PlatformVersion { get; }
string Platform { get; }
DeviceIdiom DeviceIdiom { get; }
DisplayOrientation CurrentOrientation { get; }
event EventHandler<DisplayOrientation>? OrientationChanged;
CultureInfo CurrentCulture { get; }
event EventHandler<CultureInfo>? CultureChanged;
TimeZoneInfo CurrentTimeZone { get; }
event EventHandler<TimeZoneInfo>? TimeZoneChanged;
Task<bool> SetOrientation(DisplayOrientation orientation);
Task<bool> ResetOrientation();
Task<bool> OpenBrowser(string uri, );
Task<bool> OpenMap(double latitude, double longitude, );
}
Change-detection events
Each event has its own lazy subscription — the native listener spins up when the first handler attaches and tears down when the last detaches.
| Capability | iOS / macCatalyst / macOS | Android | Windows | Bare TFM |
|---|
| Orientation | DeviceDisplay.MainDisplayInfoChanged (MAUI) | DeviceDisplay.MainDisplayInfoChanged (MAUI) | DeviceDisplay.MainDisplayInfoChanged (MAUI) | 2s poll |
| Culture | NSLocale.CurrentLocaleDidChangeNotification | BroadcastReceiver on Intent.ActionLocaleChanged | SystemEvents.UserPreferenceChanged (Locale category) | 30s poll |
| Time zone | NSSystemTimeZoneDidChangeNotification | BroadcastReceiver on Intent.ActionTimezoneChanged | SystemEvents.TimeChanged | 30s poll |
The Linux GTK4 head uses LinuxAppSupport from Shiny.Extensions.MauiHosting.Linux instead — it watches
/etc/localtime via FileSystemWatcher for time-zone changes and polls for culture and orientation.
public class SettingsViewModel(IAppSupport app)
{
public void Init()
{
app.OrientationChanged += (s, o) => { };
app.CultureChanged += (s, c) => { };
app.TimeZoneChanged += (s, tz) => { };
}
}
Orientation lock
await app.SetOrientation(DisplayOrientation.Landscape);
await app.ResetOrientation();
| Platform | Mechanism | Notes |
|---|
| Android | Activity.RequestedOrientation | Uses SensorPortrait/SensorLandscape so the device can still flip left↔right within the chosen orientation. Returns false if no current Activity |
| iOS 16+ | UIWindowScene.RequestGeometryUpdate | The active view controller must permit the requested mask via supportedInterfaceOrientations or the request is silently dropped |
| iOS 15 and earlier | Not supported | Returns false |
| macCatalyst / macOS (AppKit) / Linux (GTK4) | Not supported (desktop windows don't rotate) | Returns false |
| Windows | DisplayInformation.AutoRotationPreferences | None restores system default |
IAppStore
IAppStore looks up the latest published version from the relevant platform store, exposes deep links, and launches the review page.
public interface IAppStore
{
Task<AppStoreResult?> GetCurrent(CancellationToken cancellationToken = default);
Task<bool> OpenStore();
Task<bool> OpenReviewPage();
Task<bool> RequestReview();
}
public record AppStoreResult(
Version StoreVersion,
Version CurrentVersion,
bool NeedsUpdate,
string StoreUrl,
string? ReleaseNotes = null,
DateTimeOffset? ReleasedAt = null,
double? AverageRating = null,
long? RatingCount = null,
string? MinimumOsVersion = null
);
public class AppStoreOptions
{
public string? AppleAppId { get; set; }
public string? AppleBundleId { get; set; }
public string? AndroidPackageName { get; set; }
public string? WindowsProductId { get; set; }
public string? LinuxAppId { get; set; }
public string CountryCode { get; set; } = "us";
}
Lookup behaviour
| Platform | API | Fields populated |
|---|
| iOS / macCatalyst | iTunes Search API (itunes.apple.com/lookup?bundleId=…) | All fields — version, release notes, ratings, release date, min OS. Auto-caches trackId back into AppleAppId for subsequent deep links |
| macOS (AppKit) | Same, plus &entity=macSoftware so iTunes answers with the Mac app rather than an iOS app sharing the bundle ID | Same as iOS |
Linux (Shiny.Extensions.MauiHosting.Linux) | flatpak info for the installed version + origin, then flatpak remote-info <origin> <id>; or snap list + snap info for the tracked channel | Version, NeedsUpdate, store URL, ReleasedAt, and the Flatpak commit subject as ReleaseNotes |
| Android | Play Store HTML scrape (play.google.com/store/apps/details?id=…) with two GeneratedRegex strategies (JSON-LD softwareVersion and legacy [[["x.y.z"]]] AF_initDataCallback) | Version + NeedsUpdate only — Play HTML doesn't reliably expose other fields |
| Windows | Microsoft Store DisplayCatalog (displaycatalog.mp.microsoft.com/v7.0/products?bigIds=…) | Version, release notes (from ProductDescription), ReleasedAt where available |
| Other TFMs | Not supported | Returns null |
Deep links
| Platform | OpenStore | OpenReviewPage |
|---|
| iOS / macCatalyst | itms-apps://itunes.apple.com/app/id{AppleAppId} | itms-apps://…/app/id{AppleAppId}?action=write-review |
| macOS (AppKit) | macappstore://apps.apple.com/app/id{AppleAppId} | macappstore://…/app/id{AppleAppId}?action=write-review |
| Linux | appstream://{LinuxAppId} via xdg-open (GNOME Software / Plasma Discover / Snap Store) | Same as OpenStore — software centres show reviews on the app page |
| Android | market://details?id={packageName} | Same as OpenStore (Play Store has no separate review URL) |
| Windows | ms-windows-store://pdp/?ProductId={WindowsProductId} | ms-windows-store://review/?ProductId={WindowsProductId} |
RequestReview shows the OS's own in-app prompt: StoreKit.AppStore.RequestReview (a UIWindowScene on
iOS / Mac Catalyst 16+, the key window's NSViewController on macOS 14+) and
StoreContext.RequestRateAndReviewAppAsync on Windows. Android and Linux have no dependency-free in-app
prompt, so both fall back to OpenReviewPage.
Usage
public class UpdateChecker(IAppStore store)
{
public async Task CheckForUpdates(CancellationToken ct = default)
{
var result = await store.GetCurrent(ct);
if (result?.NeedsUpdate == true)
{
await store.OpenStore();
}
}
public Task PromptForReview() => store.OpenReviewPage();
}
:::caution
Android version detection relies on scraping the Play Store HTML. Google changes the page structure periodically — if GetCurrent returns null on Android even when the app exists, the regex likely needs updating.
:::
IStartupService
IStartupService installs the running app into the desktop operating system's startup ("launch at login") list. It is safe to call from cross-platform code — mobile reports NotSupported rather than throwing.
public interface IStartupService
{
bool IsSupported { get; }
Task<StartupServiceState> GetState(CancellationToken cancellationToken = default);
Task<StartupServiceState> Register(CancellationToken cancellationToken = default);
Task<StartupServiceState> Unregister(CancellationToken cancellationToken = default);
Task<bool> OpenSettings();
}
public enum StartupServiceState
{
NotSupported,
NotRegistered,
Enabled,
DisabledByUser,
DisabledByPolicy,
RequiresApproval
}
public class StartupServiceOptions
{
public string? Identifier { get; set; }
public string? DisplayName { get; set; }
public string? ExecutablePath { get; set; }
public IList<string> Arguments { get; set; }
}
Platform behaviour
| Platform | Mechanism | Notes |
|---|
Windows (unpackaged, WindowsPackageType=None) | HKCU\Software\Microsoft\Windows\CurrentVersion\Run | Honours ExecutablePath/Arguments. StartupApproved\Run is read so a user switching the entry off in Task Manager surfaces as DisabledByUser. OpenSettings launches ms-settings:startupapps |
| Windows (MSIX packaged) | Not supported | MSIX virtualizes HKCU writes into a per-package hive, so a Run entry never reaches the shell. Packaged apps need a windows.startupTask manifest declaration driven through WinRT, which this package doesn't implement. IsSupported is false |
macOS 13+ (Mac Catalyst and AppKit / net10.0-macos) | SMAppService.MainApp | Registers the running app bundle — Identifier, ExecutablePath and Arguments are all ignored. The first Register commonly returns RequiresApproval until the user approves it under System Settings > General > Login Items (OpenSettings opens exactly that pane). Uses DispatchQueue.MainQueue rather than MAUI's MainThread, so it works on the AppKit head |
Linux (bare net10.0 build) | ~/.config/autostart/{Identifier}.desktop (honours XDG_CONFIG_HOME) | Honours ExecutablePath/Arguments. Hidden=true or X-GNOME-Autostart-enabled=false surfaces as DisabledByUser. OpenSettings returns false — there is no cross-desktop settings page |
| iOS / Android / macOS 12 and earlier | Not supported | IsSupported is false; every call returns NotSupported |
Registering on macOS (AppKit)
Shiny.Extensions.MauiHosting multi-targets net10.0-macos. A plain AppKit app with no MauiAppBuilder
can register against the service collection instead:
services.AddStartupService(opts => opts.Identifier = "MyApp");
Both overloads live in Shiny.StartupServiceExtensions — the MauiAppBuilder one just forwards to the