Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

Platform

IPlatform is the small cross-platform surface every Shiny module uses to find a place for files and to get onto the UI thread, and that apps use to ask what they are running on. Everything is implemented natively per platform - no MAUI or Essentials - so it works the same in a MAUI app, a native app, a Linux desktop or a console. Inject it anywhere:

public interface IPlatform
{
void InvokeOnMainThread(Action action);
DirectoryInfo AppData { get; }
DirectoryInfo Cache { get; }
DirectoryInfo Public { get; }
Version AppVersion { get; }
string? ApplicationIdentifier { get; }
string DeviceManufacturer { get; }
string DeviceModel { get; }
Version? PlatformVersion { get; }
string Platform { get; }
DeviceFormFactor DeviceFormFactor { get; }
CultureInfo CurrentCulture { get; }
event EventHandler<CultureInfo>? CultureChanged;
TimeZoneInfo CurrentTimeZone { get; }
event EventHandler<TimeZoneInfo>? TimeZoneChanged;
void OpenAppSettings();
}
Platform AppVersion DeviceManufacturer / DeviceModel PlatformVersion Platform DeviceFormFactor
Android Package versionName Build.MANUFACTURER / Build.MODEL Build.VERSION.RELEASE Android UiModeManager (TV, Car, Watch, Desk), else Tablet from 600dp smallest width, else Phone
iOS / tvOS CFBundleShortVersionString Apple / hardware identifier (iPhone16,1; the simulated model on a simulator) UIDevice.SystemVersion iOS / tvOS UIDevice.UserInterfaceIdiom
Mac Catalyst CFBundleShortVersionString Apple / hw.model (Mac15,3) UIDevice.SystemVersion MacCatalyst Desktop
macOS (AppKit) CFBundleShortVersionString Apple / hw.model NSProcessInfo.OperatingSystemVersion macOS Desktop
Windows Package version, or the entry assembly’s when unpackaged EasClientDeviceInformation system manufacturer / product name Environment.OSVersion Windows AnalyticsInfo device family
Linux Entry assembly version DMI sys_vendor / product_name, or the device-tree model on a Raspberry Pi Environment.OSVersion Linux Desktop
Blazor WebAssembly Entry assembly version Unknown null Browser Unknown

ApplicationIdentifier is the app’s store/package identifier, or null where the app has none:

Platform ApplicationIdentifier
Android Package name (applicationId)
iOS / tvOS / Mac Catalyst / macOS Bundle identifier (CFBundleIdentifier)
Windows Package identity name when packaged, null when unpackaged
Linux FLATPAK_ID or SNAP_NAME inside a Flatpak or Snap, otherwise null
Blazor WebAssembly / other plain .NET null

DeviceFormFactor is Shiny.DeviceFormFactor - Unknown, Phone, Tablet, Desktop, TV, Watch or Car.

Plain net10.0 has no hosting layer to register IPlatform, so a Blazor app does it itself:

using Shiny.Infrastructure;
builder.Services.AddShinyCoreServices(); // IPlatform (NetPlatform) and the key/value stores

The browser-only extras live on the concrete NetPlatform - inject it directly:

Member Notes
UserAgent navigator.userAgent, read once through the runtime’s own JavaScript interop (no script file). null outside a browser, including Blazor Server
UserAgentVersion A best-effort browser version parsed from the user agent - Edge, Opera, Firefox, Chrome, then Safari, most specific first - or null

OpenAppSettings() opens the app’s own page in the system settings on Android, iOS, Mac Catalyst and packaged Windows apps, and the system settings themselves elsewhere (System Settings on macOS, GNOME Settings or Plasma System Settings on Linux). It does nothing in the browser.

CurrentCulture and CurrentTimeZone always read the live value. CultureChanged and TimeZoneChanged start their native listener with the first subscriber and stop it with the last, so an app that never subscribes pays nothing. They are raised only when the value actually changes, on whatever thread the OS reports it - marshal with InvokeOnMainThread before touching UI.

Platform Culture Time zone
Android ACTION_LOCALE_CHANGED broadcast ACTION_TIMEZONE_CHANGED broadcast
iOS / tvOS / Mac Catalyst / macOS NSCurrentLocaleDidChangeNotification NSSystemTimeZoneDidChangeNotification
Windows SystemEvents.UserPreferenceChanged (Locale) SystemEvents.TimeChanged
Linux 30-second poll - a Linux locale switch only applies at the next login /etc/localtime file watch, falling back to a 30-second poll
Blazor WebAssembly / other plain .NET 30-second poll 30-second poll

IScreenInfo is registered next to IPlatform by every host. It reports the main display and locks the app’s orientation, natively and without MAUI.

public class VideoPage(IScreenInfo screen)
{
public async Task EnterFullScreen()
{
var m = screen.Metrics; // ScreenMetrics(Width, Height, Density, Orientation, RefreshRate)
Console.WriteLine($"{m.WidthIndependent}x{m.HeightIndependent} @ {m.RefreshRate}Hz");
screen.OrientationChanged += (_, orientation) => { /* off the UI thread - marshal first */ };
var locked = await screen.SetOrientation(DeviceOrientation.Landscape);
// ...
await screen.ResetOrientation();
}
}

Width and Height are physical pixels as the display is currently oriented, and WidthIndependent / HeightIndependent divide them by Density. MetricsChanged and OrientationChanged start watching with the first subscriber and stop with the last, and are raised only when something actually changed. The orientation enum is Shiny.DeviceOrientation (Unknown, Portrait, Landscape) - named so it does not clash with MAUI’s DisplayOrientation or Android’s ScreenOrientation.

Platform Metrics Changes SetOrientation
Android Default display’s real size, density and refresh rate DisplayManager listener The current Activity’s requested orientation (sensor portrait / landscape)
iOS UIScreen.MainScreen Device rotation, re-read once the interface has turned, and screen mode changes iOS 16+ through RequestGeometryUpdate - see below
Mac Catalyst / tvOS UIScreen.MainScreen Screen mode changes Not supported (returns false)
macOS NSScreen.MainScreen Screen parameter and window screen changes Not supported
Windows Primary monitor’s current mode and system DPI SystemEvents.DisplaySettingsChanged Auto-rotation preference - only devices that rotate honour it
Blazor WebAssembly window.screen × devicePixelRatio, Screen Orientation API resize and orientation change screen.orientation.lock() - only for a full-screen page or installed app
Linux / other plain .NET ScreenMetrics.Empty Never Not supported

For foldables and dual-screen devices - where the fold is, tabletop and book posture - see Screen Posture.

On Blazor WebAssembly the browser viewport (window.innerWidth / innerHeight) is on the concrete BrowserScreenInfo (namespace Shiny.Infrastructure) as ViewportWidth / ViewportHeight. Its JS module loads on first use - UseShiny() loads it up front - and Metrics is ScreenMetrics.Empty until then.

Platform AppData Cache Public
Android Context.FilesDir Context.CacheDir GetExternalFilesDir(null) (can be null if external storage is unavailable)
iOS / tvOS / Mac Catalyst Library Library/Caches Documents
macOS (AppKit) Library Library/Caches Documents
Windows (packaged) ApplicationData.Current.LocalFolder LocalFolder/Cache LocalFolder/Public
Windows (unpackaged) / plain .NET LocalApplicationData/<entry assembly> …/Cache …/Public

On Linux, LocalApplicationData resolves to $XDG_DATA_HOME (falling back to ~/.local/share), so the plain .NET layout already follows XDG. The folder is named after the entry assembly so that apps don’t write into a directory the whole desktop shares.

ResourceToFilePath(assembly, resourceName) copies an embedded resource into AppData the first time it is asked for, then returns the path. Use it for native APIs that only accept a file, such as a bundled database or model.

InvokeOnMainThread runs the action inline if you are already on the main thread and posts it otherwise. The extensions in PlatformExtensions add async versions that complete when the work has run and pass exceptions back to the caller:

await platform.InvokeOnMainThreadAsync(() => label.Text = "Done");
var result = await platform.InvokeOnMainThreadAsync(() => ReadSomeUiState());
// async work that must start on the UI thread
await platform.InvokeTaskOnMainThread(async () => await ShowDialogAsync(), cancellationToken);

Windows and plain .NET have no toolkit-independent main thread, so WindowsPlatform.MainThreadHandler and NetPlatform.MainThreadHandler are static hooks for the hosting layer to set. UseShiny() sets the Windows hook to the WinUI dispatcher, and in a MAUI app on a non-platform backend such as Linux GTK it sets the NetPlatform hook to that backend’s MAUI dispatcher. A GTK or other desktop head without MAUI should set the NetPlatform hook to its own main loop:

NetPlatform.MainThreadHandler = action => GLib.Idle.Add(() => { action(); return false; });

If nothing sets the handler, the action runs inline. Code with no UI thread still works; it just isn’t marshalled.

Resolve the concrete AndroidPlatform (or use Host.Platform) for the Android-specific helpers Shiny’s modules are built on.

var result = await platform.RequestPermissions(
Manifest.Permission.AccessFineLocation,
Manifest.Permission.AccessCoarseLocation
);
if (!result.IsSuccess())
{
var fineGranted = result.IsGranted(Manifest.Permission.AccessFineLocation);
}
// single permission, mapped to AccessState
var state = await platform.RequestAccess(Manifest.Permission.Camera);
// no prompt, just the current state
var current = platform.GetCurrentPermissionStatus(Manifest.Permission.Camera);
  • If every permission is already granted, the request returns immediately without showing a dialog.
  • A request needs a foreground Activity. If none is up yet (for example, the request fired during startup), Shiny waits up to 5 seconds for one to resume and then throws TimeoutException. That timeout applies only to getting an activity. Once the dialog is showing, the user can take as long as they want. Pass a CancellationToken to give up yourself.
  • GetCurrentPermissionStatus returns Unknown rather than Denied for a permission the app has never requested. Shiny records requested permissions in the default store so that the distinction survives a restart.
  • RequestFilteredPermissions(params AndroidPermission[]) and EnsureAllManifestEntries(...) take AndroidPermission(permission, minSdk, maxSdk) records and skip entries that don’t apply to the running API level. Use them for permissions that were added or retired across Android versions.
  • IsInManifest(permission) checks the merged manifest.
Member Description
CurrentActivity The most recently active Activity, or null
ActivityChanged Event raised with an ActivityChanged(Activity, ActivityState, Bundle?) for Created, Started, Resumed, Paused, Stopped, SaveInstanceState and Destroyed
WaitForActivity(state, ct) Completes the next time any activity reaches state (Resumed by default)
StartActivityForResult(intent, ct) Starts an activity from the current one and awaits its ActivityResult(Activity, RequestCode, ResultCode, Data) - a role request, a settings screen, a consent dialog. Waits up to 5s for an activity to appear; no limit on the user
ActivityResultReceived Event raised with every ActivityResult the host forwards, whoever started the activity

Activity results reach Shiny through the host: Shiny.Hosting.Maui forwards them automatically, and with Shiny.Hosting.Native the activity derives from ShinyAndroidActivity. To handle them from your own service instead of the event, implement IAndroidLifecycle.IOnActivityResult and register it in DI.

var roles = (RoleManager)platform.AppContext.GetSystemService(Context.RoleService)!;
var result = await platform.StartActivityForResult(roles.CreateRequestRoleIntent(RoleManager.RoleCallScreening));
if (result.IsOk)
{
// the user granted the role
}
Member Description
AppContext The Android Application
StartService(type, stopWithTask) / StopService(type) Start or stop a Shiny foreground service. Uses StartForegroundService on API 31+
RequestForegroundServicePermissions() Requests FOREGROUND_SERVICE and, on API 33+, POST_NOTIFICATIONS. Returns Restricted when only the notification permission was refused
CreateIntent<T>(actions) / GetBroadcastPendingIntent<T>(...) Intent helpers. GetPendingIntentFlags adds Mutable on API 31+
RegisterBroadcastReceiver<T>(exported, actions) Registers a receiver with the exported flag Android 14 requires
GetSystemService<T>(name) / GetSystemServiceValue(...) Typed system-service access
GetNotificationIconResource() Returns the notification drawable, or the app icon if there isn’t one. Throws if neither exists
GetResourceIdByName, GetColorByName, GetRawResourceIdByName Resource lookups by name

AppleExtensions collects the helpers Shiny’s Apple backends use:

Member Description
IsSimulator true on the iOS/tvOS simulator, always false on Mac Catalyst/macOS
HasBackgroundMode("location") Checks UIBackgroundModes in Info.plist
AssertInfoPlistEntry(key, assert) / HasPlistValue(key, ifVersion) Info.plist checks, optionally only from an OS version onward
HasAppDelegateHook(selector) / AssertAppDelegateHook(...) Checks that the AppDelegate implements a selector, for example didRegisterForRemoteNotificationsWithDeviceToken
ToDateTime() / ToNSDate() NSDate conversions (Apple’s reference date is 2001-01-01 UTC)
ToGuid() / ToNSUuid() NSUuid conversions
FromNsDictionary() / ToNsDictionary() String dictionary conversions

Shiny.Permissions.IsInMainfest(capability) reads AppxManifest.xml to check whether a DeviceCapability is declared. It only applies to packaged apps.