.NET MAUI (C# / .NET)
Microsoft's cross-platform UI framework and the successor to Xamarin.Forms: it wraps each platform's native UI stack behind a thin C# handler abstraction and owns essentially none of the windowing layer itself — there is no MAUI window server, no MAUI event loop, and (deliberately) no Linux desktop target.
| Field | Value |
|---|---|
| Version/commit studied | dotnet/maui @ c389325ecfebf1d07b03d9a928ebf921a78670c7 (main, June 8, 2026) |
| Language | C# (.NET 8/9/10); platform code compiled per-TFM with #if guards |
| License | MIT |
| Repository | dotnet/maui |
| Documentation | Supported platforms / Handlers |
| Category | Native-control-wrapping framework (delegating windowing to per-platform UI stacks) |
| Platforms covered | Windows (WinUI 3 / Windows App SDK), macOS (Mac Catalyst, not AppKit), iOS, Android, Tizen (Samsung) |
| Loop ownership | None — MAUI never owns a loop; it lives inside CFRunLoop/NSRunLoop (iOS/Catalyst), the Win32 message pump (WinUI 3), and the Android Looper/Handler queue |
| Repo paths (platform) | src/Core/src/Platform/{Windows,iOS,Android,Tizen}/, src/Core/src/Handlers/, src/Core/src/Dispatching/ |
NOTE
This is the opposite philosophy to Avalonia. Where Avalonia draws every pixel itself and talks to Win32/X11/Wayland/AppKit directly, MAUI instantiates native widgets (UITextField, Android.Widget.EditText, WinUI TextBox) and lets the underlying OS run the window, the loop, input, IME, and decorations. The interesting findings here are therefore about what is gained and lost by not owning the windowing layer — and about the seams where that abstraction leaks.
Overview
What it solves
.NET MAUI lets a single C# codebase target four OS UI stacks. The framework's own positioning makes the wrapping stance explicit — from docs/supported-platforms.md:
.NET Multi-platform App UI (.NET MAUI) apps can be written for the following platforms:
- Android 5.0 (API 21) or higher is required.
- iOS 12.2 or higher is required.
- macOS 12 or higher, using Mac Catalyst.
- Windows 11 and Windows 10 version 1809 or higher, using Windows UI Library (WinUI) 3.
Three load-bearing facts hide in that list: macOS is reached via Mac Catalyst (the UIKit-on-Mac compatibility layer), not AppKit; Windows is WinUI 3 / Windows App SDK, not raw Win32 or WPF; and Linux is simply not there. Tizen is mentioned separately as "Additional platform support … provided by Samsung." MAUI's job is to map a portable IWindow/IView tree onto whatever each of those stacks calls a window and a view.
Design philosophy
- Wrap, don't reimplement. Every cross-platform element (
IWindow,IButton,IEntry) has a per-platform handler that creates and drives a real native control. The window is no exception: the handler'sPlatformViewis literallyMicrosoft.UI.Xaml.Window,UIKit.UIWindow, orAndroid.App.Activity— see theusing PlatformView =aliases inWindowHandler.cs. - No loop, no surface, no decorations of its own. Because the platform owns the window, it also owns the event loop, frame pacing, DPI, IME, server-side vs client-side decorations (CSD vs SSD), and the no-buffer-no-window dance on Wayland — none of which MAUI ever sees, because MAUI never runs on Wayland.
- A property/command mapper instead of a render tree. State flows one-way: a virtual-view property change is dispatched through a
PropertyMapperto a staticMapXxxmethod that pokes the native control. This is the architectural heir to Xamarin.Forms' renderers, redesigned as the lighter handler model (see §10). - The Dispatcher is the only loop primitive MAUI exposes, and it is a thin shim over the native main-thread queue (
DispatchQueue.MainQueue, WinUIDispatcherQueue, AndroidHandler).
IMPORTANT
Because MAUI delegates the windowing layer wholesale, several dimensions of this study (Wayland specifics, X11 selections, raw pointer motion, frame-callback vsync) do not apply at the framework level. That absence is itself the central finding and is documented under each heading rather than skipped.
How it works
The handler architecture
The base abstraction is ElementHandler (src/Core/src/Handlers/Element/ElementHandler.cs) and its generic subclass ElementHandler<TVirtualView, TPlatformView>. A handler owns two objects: the cross-platform VirtualView (an IElement) and the native PlatformView (object). SetVirtualView lazily creates the platform view and runs ConnectHandler:
// src/Core/src/Handlers/Element/ElementHandler.cs (SetVirtualView, abridged)
VirtualView = view;
if (PlatformView is null)
{
_handlerState = ElementHandlerState.Connecting;
PlatformView = CreatePlatformElement(); // instantiates the native control
}
// ...
if (setupPlatformView)
ConnectHandler(PlatformView); // wires native events
_mapper.UpdateProperties(this, VirtualView); // pushes every mapped propertyProperty changes are routed through a static PropertyMapper. For windows, WindowHandler.cs declares the map; note how it is conditionally compiled per platform — MaximumWidth/TitleBar only exist on Windows + Mac Catalyst, FlowDirection/IsMinimizable only on Windows:
// src/Core/src/Handlers/Window/WindowHandler.cs
public static IPropertyMapper<IWindow, IWindowHandler> Mapper = new PropertyMapper<IWindow, IWindowHandler>(ElementHandler.ElementMapper)
{
[nameof(IWindow.Title)] = MapTitle,
[nameof(IWindow.Content)] = MapContent,
[nameof(IWindow.X)] = MapX,
// ...
#if WINDOWS || MACCATALYST
[nameof(IWindow.MaximumWidth)] = MapMaximumWidth,
[nameof(IWindow.TitleBar)] = MapTitleBar,
#endif
#if WINDOWS
[nameof(IWindow.FlowDirection)] = MapFlowDirection,
[nameof(IWindow.IsMinimizable)] = MapIsMinimizable,
#endif
};The platform-view alias is the whole story
The single most revealing file is WindowHandler.cs, whose header type alias declares exactly what a "MAUI window" is per platform:
// src/Core/src/Handlers/Window/WindowHandler.cs
#if __IOS__ || MACCATALYST
using PlatformView = UIKit.UIWindow;
#elif MONOANDROID
using PlatformView = Android.App.Activity;
#elif WINDOWS
using PlatformView = Microsoft.UI.Xaml.Window;
#elif TIZEN
using PlatformView = Tizen.NUI.Window;
#endifThere is no #elif LINUX. The desktop-fallback handler, WindowHandler.Standard.cs, throws:
// src/Core/src/Handlers/Window/WindowHandler.Standard.cs
protected override object CreatePlatformElement() => throw new NotImplementedException();So a non-mobile, non-Windows build has no window at all. The same NotImplementedException pattern repeats in Dispatcher.Standard.cs — MAUI has no loop where it has no platform.
1. Window creation & lifecycle
There is no portable "create window" call. Each platform constructs its native window object and hands it to a MauiContext window scope.
| Platform | Native window type | Creation call | Creation site |
|---|---|---|---|
| Windows | Microsoft.UI.Xaml.Window (subclassed MauiWinUIWindow) | new MauiWinUIWindow(); …; winuiWindow.Activate(); | ApplicationExtensions.cs |
| iOS / Catalyst | UIKit.UIWindow | new UIWindow(windowScene); then MakeKeyAndVisible() | ApplicationExtensions.cs |
| Android | Android.App.Activity (the Activity is the "window") | Activity.SetContentView(rootView) | WindowHandler.Android.cs |
| Tizen | Tizen.NUI.Window | resolved from DI; SetContent(...) | WindowHandler.Tizen.cs |
| Linux/desktop | — | throw new NotImplementedException() | WindowHandler.Standard.cs |
On Windows, MauiWinUIWindow derives from Microsoft.UI.Xaml.Window and is created with new MauiWinUIWindow() then .Activate() (ApplicationExtensions.cs, CreatePlatformWindow). Its constructor immediately reaches for the Windows App SDK AppWindow and AppWindowTitleBar to extend content into the title bar and apply a Mica backdrop:
// src/Core/src/Platform/Windows/MauiWinUIWindow.cs (constructor, abridged)
if (AppWindowTitleBar.IsCustomizationSupported())
{
var titleBar = this.GetAppWindow()?.TitleBar;
if (titleBar is not null)
titleBar.ExtendsContentIntoTitleBar = true;
}
if (MicaController.IsSupported())
base.SystemBackdrop = new MicaBackdrop() { Kind = MicaKind.BaseAlt };
SubClassingWin32(); // installs a custom WndProc — see §2 and §9On iOS/Catalyst, the lifecycle is driven by UIKit's scene/app-delegate callbacks (see §2); CreatePlatformWindow builds a UIWindow and calls MakeKeyAndVisible() (ApplicationExtensions.cs). On Android, the Activity is the window; MapContent calls Activity.SetContentView(rootView) (WindowHandler.Android.cs).
Window-attributes model. The portable IWindow exposes X/Y/Width/Height, plus Minimum*/Maximum*/IsMinimizable/IsMaximizable/TitleBar — but only some platforms implement each. The mapper in WindowHandler.cs gates size/min-max/title-bar behind #if WINDOWS || MACCATALYST and minimizable/maximizable/flow-direction behind #if WINDOWS. On Android and iOS, X/Y/size are largely no-ops because the OS positions and sizes app windows; the mapper still runs but the native side often ignores it (e.g. iOS UpdateX/UpdateY exist but a phone window fills the screen). Positioning on Windows goes through WindowExtensions.UpdatePosition, which calls AppWindow.Move, and sizing calls AppWindow.Resize; minimize/maximize state flows through the OverlappedPresenter:
// src/Core/src/Platform/Windows/WindowExtensions.cs (UpdateIsMinimizable)
if (appWindow?.Presenter is UI.Windowing.OverlappedPresenter presenter)
presenter.IsMinimizable = window.IsMinimizable;Surface/handle exposure. Because the platform owns rendering, there is no MAUI-level GPU surface or raw-window-handle equivalent. The closest analogue is WindowExtensions.GetWindowHandle (WindowExtensions.cs), which returns the Win32 HWND via WinRT.Interop.WindowNative.GetWindowHandle — the documented retrieve-an-HWND escape hatch (see §9).
Destruction ordering. ElementHandler.DisconnectHandler nulls PlatformView before calling the platform DisconnectHandler(oldPlatformView) so nobody re-enters a half-torn-down handler (ElementHandler.cs). On Windows, MauiWinUIWindow.OnClosedPrivate unhooks Activated/Closed/VisibilityChanged, destroys the window icon via the DestroyIcon P/Invoke, and clears the back-reference — a hand-rolled teardown because the underlying Win32 resources are unmanaged.
2. Event loop
MAUI owns no event loop. This is the defining consequence of the wrapping design: the loop belongs to the host UI stack, and MAUI code runs only as callbacks the platform invokes.
iOS / Mac Catalyst — the loop is UIKit's, backed by
CFRunLoop/NSRunLoop. MAUI plugs in via the application delegate (MauiUIApplicationDelegate.cs) and, for multi-window, the scene delegate (MauiUISceneDelegate.cs). Each native callback is[Export(...)]-ed and fans the event out to registered lifecycle handlers:cs// src/Core/src/Platform/iOS/MauiUIApplicationDelegate.cs [Export("application:didFinishLaunchingWithOptions:")] public virtual bool FinishedLaunching(UIApplication application, NSDictionary? launchOptions) { _application = _services!.GetRequiredService<IApplication>(); this.SetApplicationHandler(_application, _applicationContext); if (!this.HasSceneManifest()) this.CreatePlatformWindow(_application, application, launchOptions); _services?.InvokeLifecycleEvents<iOSLifecycle.FinishedLaunching>(del => del(application!, launchOptions!)); return true; }Windows — the loop is the Win32 message pump that WinUI 3 / the Windows App SDK runs. MAUI's
MauiWinUIApplication.OnLaunched(MauiWinUIApplication.cs) creates the app and the first window. To intercept native messages (the Win32 modal resize/move loop is part of this pump), MAUI subclasses theWndProc— see below and §9.Android — the loop is the Android main-thread
Looper; theActivitylifecycle callbacks drive MAUI.
Win32 message interception. MauiWinUIWindow.SubClassingWin32 (MauiWinUIWindow.cs) routes raw window messages through MAUI so it can implement min/max-size clamping (WM_GETMINMAXINFO) and react to title-bar style changes (WM_STYLECHANGING):
// src/Core/src/Platform/Windows/MauiWinUIWindow.cs (OnWindowMessage, abridged)
if (e.MessageId == PlatformMethods.MessageIds.WM_GETMINMAXINFO)
{
var win = this as IPlatformSizeRestrictedWindow;
var rect = Marshal.PtrToStructure<PlatformMethods.MinMaxInfo>(e.LParam);
// clamp rect.MinTrackSize / rect.MaxTrackSize to the user's Minimum*/Maximum*
Marshal.StructureToPtr(rect, e.LParam, true);
}The subclassing itself is done by WindowMessageManager (WindowMessageManager.windows.cs), which swaps the window procedure with SetWindowLongPtr(GWL_WNDPROC, …) and chains to the original via CallWindowProc — the standard Win32 subclassing dance, exposed to apps as a lifecycle event (see §9).
Timers, wakeups & cross-thread injection. MAUI's only loop-facing primitive is the IDispatcher. Each platform's Dispatcher wraps the native main-thread queue:
// src/Core/src/Dispatching/Dispatcher.iOS.cs
bool DispatchImplementation(Action action)
{
_dispatchQueue.DispatchAsync(() => action()); // Grand Central Dispatch main queue
return true;
}
// src/Core/src/Dispatching/Dispatcher.Windows.cs
bool DispatchImplementation(Action action) =>
_dispatcherQueue.TryEnqueue(() => action()); // WinUI DispatcherQueue
// src/Core/src/Dispatching/Dispatcher.Android.cs
bool DispatchImplementation(Action action) =>
_dispatcher.Post(() => action()); // Android Handler/LooperIsDispatchRequired checks whether the caller is off the UI thread (DispatchQueue.CurrentQueueLabel on iOS; DispatcherQueue.HasThreadAccess on Windows), so cross-thread "user-event injection" is just enqueueing onto the native main queue. DispatcherTimer likewise delegates to DispatchAfter (iOS), DispatcherQueueTimer (Windows), or Handler.PostDelayed (Android).
Frame pacing & vsync. MAUI does not pace frames — the native compositor does. There is no MAUI access to Wayland frame callbacks, CVDisplayLink/CADisplayLink, or DXGI waitable swapchains; redraw coalescing happens inside WinUI's composition, Core Animation, and the Android view system, below MAUI's floor. Where this study's sibling subjects (winit, sokol, SDL3) must explicitly drive vsync, MAUI is entirely passive.
NOTE
The readiness-vs-completion axis used elsewhere in this catalog does not apply: MAUI never multiplexes file descriptors or a display connection. Its concurrency story is the .NET thread pool plus the native UI dispatcher, not an I/O reactor — cross-link async-io for the runtime side.
3. Input
MAUI's input model is almost entirely delegated to native controls. It does not implement a scancode/keysym translator, an xkbcommon state machine, key-repeat synthesis, or a compose/dead-key engine — those live in UIKit, the Android input stack, and Win32/WinUI, which deliver already-cooked text to the native widgets MAUI hosts.
The "Keyboard" abstraction is the soft keyboard, not physical keys. MAUI's portable Keyboard type selects an on-screen keyboard variant / input scope, mapped to native enums. On iOS it sets UIKeyboardType/autocapitalization on the native text input (KeyboardExtensions.cs); on Windows it builds a WinUI InputScope (KeyboardExtensions.cs):
// src/Core/src/Platform/iOS/KeyboardExtensions.cs
else if (keyboard == Keyboard.Email) textInput.SetKeyboardType(UIKeyboardType.EmailAddress);
else if (keyboard == Keyboard.Numeric) textInput.SetKeyboardType(UIKeyboardType.DecimalPad);
else if (keyboard == Keyboard.Telephone) textInput.SetKeyboardType(UIKeyboardType.PhonePad);Physical keys exist only as menu accelerators. The one place MAUI touches the scancode/keysym/virtual-key model is KeyboardAccelerator, used for menu shortcuts. On Windows it maps to the WinUI VirtualKey/VirtualKeyModifiers enums (KeyboardAcceleratorExtensions.cs):
// src/Core/src/Platform/Windows/KeyboardAcceleratorExtensions.cs
accelerator.Key = key.ToVirtualKey(); // string -> Windows.System.VirtualKey
accelerator.Modifiers = modifiers.ToVirtualKeyModifiers();The same file records a hard limitation in a comment: "Gamepad virtual keys are not supported." There is no general per-keystroke KeyDown/KeyUp surface in MAUI core; apps that need raw key events reach into the native control via the handler (see §9).
IME / text input / composition. MAUI implements none of the pre-edit/composition machinery itself — no zwp_text_input_v3, no Windows TSF/IMM32 wiring, no NSTextInputClient, no XIM. Composition, candidate windows, and dead keys are handled by the native widget (UITextField, WinUI TextBox, Android EditText), which already speaks the platform IME. MAUI only observes the resulting text via TextChanged. The one IME-adjacent feature MAUI does own is keyboard avoidance: KeyboardAutoManager (KeyboardAutoManager.cs, KeyboardAutoManagerScroll.cs) scrolls the focused field above the iOS soft keyboard.
Pointer, scroll, touch, gestures. Gesture recognition is layered on top of native pointer events, not raw motion. On Windows, GesturePlatformManager (GesturePlatformManager.Windows.cs) subscribes to WinUI PointerPressed/PointerMoved/PointerReleased and ManipulationDelta, tracking fingers by PointerRoutedEventArgs.Pointer.PointerId and synthesizing pan/pinch/swipe — so absolute vs relative/raw motion, high-resolution scroll (wl_pointer axis_v120, WM_MOUSEWHEEL accumulation, macOS momentum phases), and pointer capture are all resolved by the native stack before MAUI sees them. There is no MAUI pointer-confinement or pointer-lock API.
Cursor. Cursor handling is delegated; MAUI exposes a PointerOver visual state and lets the native control pick the cursor shape. There is no cursor_shape_v1-style choice at the MAUI layer because MAUI never renders on Wayland.
4. Wayland specifics
WARNING
Not applicable: .NET MAUI has no Wayland, X11, or any Linux desktop backend. There is no xdg-shell, no xdg-decoration, no libdecor, no fractional-scale-v1/viewporter/xdg-activation/layer-shell, and no compositor-specific (mutter/kwin/sway/weston) workaround anywhere in the tree — because the WindowHandler.Standard.cs fallback throws NotImplementedException on any non-iOS/Android/Windows/Tizen target.
The absence is by design and is the most-requested missing feature (see §10). The closest the ecosystem gets to Wayland/X11 is the community Maui.Gtk project (a GTK4 back-end via GirCore bindings, covered by Phoronix), which is out of tree and unofficial. Tizen — the only non-Microsoft, non-Apple-or-Google desktop-ish target — uses Tizen.NUI.Window (WindowHandler.Tizen.cs); NUI is Samsung's own compositor-backed UI toolkit, not Wayland-protocol code that MAUI authored. Server-side vs client-side decoration (CSD vs SSD) is therefore decided entirely by the host stack (DWM on Windows, the compositor on Tizen), never by MAUI.
5. DPI & scaling
MAUI's portable coordinates are device-independent units (DIPs) — logical, not physical. Each platform converts to physical pixels using a scale factor it queries from the OS; MAUI never owns the awareness model.
Windows —
WindowExtensions.GetDisplayDensity(WindowExtensions.cs) computes the factor from the Win32GetDpiForWindowdivided byDeviceDisplay.BaseLogicalDpi(96):cs// src/Core/src/Platform/Windows/WindowExtensions.cs return PlatformMethods.GetDpiForWindow(hwnd) / DeviceDisplay.BaseLogicalDpi;All window geometry is multiplied by this density before being handed to
AppWindow.Move/AppWindow.Resize, and the reverse division is applied inUpdateVirtualViewFrameto report logical coordinates back (WindowHandler.Windows.cs). Per-monitor DPI awareness (v2) and theWM_DPICHANGEDdance are handled inside WinUI 3 / the Windows App SDK; MAUI does not processWM_DPICHANGEDitself (it is not in theWindowMessageManagerswitch).iOS / Catalyst — backing scale is UIKit's
contentScaleFactor/UIScreen.scale; MAUI reads frame geometry already in points.Android — density comes from the
Activity's resources;GetDisplayDensityreturns the device's scaled density.
Fractional scaling on Wayland and the "created-at-wrong-scale-then-rescaled" problem are not MAUI concerns — there is no Wayland, and on the supported platforms the native window arrives already at the correct scale, with the OS firing native resize/DPI events that MAUI observes (e.g. the Catalyst effectiveGeometry KVO observer in WindowHandler.iOS.cs). Mixed-DPI multi-monitor migration is likewise resolved by WinUI/UIKit before MAUI's FrameChanged runs.
6. Multi-window & popups
Multi-window is supported on the desktop-class targets by asking the native windowing system to spawn a window; MAUI never stacks or grabs windows itself.
- Windows —
ApplicationHandler.MapOpenWindow(ApplicationHandler.Windows.cs) callsCreatePlatformWindow, which doesnew MauiWinUIWindow(); winuiWindow.Activate();(ApplicationExtensions.cs).MapCloseWindow/MapActivateWindowcall WinUIWindow.Close()/Window.Activate(). - iOS / Catalyst — multi-window means multiple
UIScene/UIWindowScenes.RequestNewWindow(ApplicationExtensions.cs) callsUISceneSessionActivationRequest(iOS 17+) orUIApplication.RequestSceneSessionActivation, and the new scene is wired up inMauiUISceneDelegate.WillConnect(MauiUISceneDelegate.cs). - Android / Tizen — single-window-centric;
OpenWindowRequest(OpenWindowRequest.cs) carries only persisted state, with no Windows-styleLaunchActivatedEventArgs.
Modal dialogs, tooltips, menus, and popups are native. MAUI uses native flyouts/menus (MenuFlyout, UIMenuSystem) and modal page presentation; it does not implement xdg_popup grab semantics or X11 override-redirect — popup stacking, grabs, and dismissal are the platform's job. Parent/child stacking and window groups are likewise whatever WinUI/UIKit provide.
7. Threading
The threading model is dictated by the native stacks MAUI wraps, and it is the familiar one: the UI thread is sacred.
Windows must create and touch the window on the thread that owns its
DispatcherQueue(WinUI 3's single-threaded apartment for UI).Dispatcher.WindowschecksDispatcherQueue.HasThreadAccessto decide whether marshalling is required (Dispatcher.Windows.cs).iOS / Mac Catalyst force the main thread because UIKit (and hence Mac Catalyst) is main-thread-only — the recurring "main-thread AppKit/UIKit" constraint.
DispatcherProvider.GetForCurrentThreadImplementationreturns a dispatcher only when the current GCD queue is theDispatchQueue.MainQueue(Dispatcher.iOS.cs):cs// src/Core/src/Dispatching/Dispatcher.iOS.cs var q = DispatchQueue.CurrentQueue; if (q != DispatchQueue.MainQueue) return null; return new Dispatcher(q);Android events arrive on the main
Looperthread; off-thread work marshals back via theHandler.
Events are delivered on the UI thread by the platform. Rendering off the event thread is possible only to the extent the native stack allows it (e.g. WinUI composition and Core Animation composite on their own threads), but that is invisible to MAUI — MAUI's own work (handler mapping, layout) runs on the UI thread. Background work uses ordinary .NET tasks and marshals UI updates through IDispatcher.Dispatch.
8. Clipboard & DnD
Clipboard is a thin wrapper over each platform's native data-transfer API (in MAUI Essentials), not a hand-rolled selection protocol.
Windows — uses the WinRT
DataPackage/Clipboard(Clipboard.windows.cs). MIME negotiation and the Win32 delayed-rendering protocol are handled inside WinRT; MAUI just sets/gets text and subscribes toContentChanged:cs// src/Essentials/src/Clipboard/Clipboard.windows.cs var dataPackage = new DataPackage(); dataPackage.SetText(text); WindowsClipboard.SetContent(dataPackage);iOS / Catalyst —
UIPasteboard; macOS Essentials usesNSPasteboard(Clipboard.macos.cs, a legacy AppKit path retained for Essentials).Android —
ClipboardManager.
The portable IClipboard surface is text-centric (SetTextAsync/GetTextAsync/HasText); rich formats, the Wayland selection model, Win32 delayed rendering, and X11 INCR are entirely below MAUI's API — and X11/Wayland never appear because there is no Linux backend. Drag-and-drop is exposed at the Controls layer via gesture recognizers backed by the native DnD stacks (WinUI DragStarting, UIKit drag interactions); MAUI authors no transfer protocol.
9. Escape hatches
Because the abstraction is thin, the escape hatches are short — and they reveal exactly where it leaks.
- Native view access via the handler.
handler.PlatformViewis the native control (UIWindow, WinUIWindow,Activity).IElementHandler.PlatformView(IElementHandler.cs) is the documented door to the underlying widget; for raw key events, custom drawing, or platform tweaks, apps cast it and use it directly. HWNDaccess.WindowExtensions.GetWindowHandle(WindowExtensions.cs) returns the Win32 handle (the retrieve-an-HWND pattern) so apps can calluser32/shell32directly — MAUI itself uses it forGetDpiForWindow,ShowWindow, and icon extraction.WndProcsubclassing / raw Win32 message hook. The most powerful hatch: apps subscribe to theOnPlatformMessagelifecycle event, which fires for every window message becauseMauiWinUIWindowsubclasses the window procedure viaWindowMessageManager(WindowMessageManager.windows.cs). The manager swapsGWL_WNDPROCwithSetWindowLongPtrand chains the original withCallWindowProc— letting apps mark a messageHandledand return their own result. The existence of this hook is an admission that the WinUI surface is sometimes insufficient.- Per-platform lifecycle events.
ConfigureLifecycleEventsexposesiOSLifecycle/WindowsLifecycle/AndroidLifecycledelegates (e.g.FinishedLaunching,OnPlatformWindowSubclassed,OnLaunched) so apps can run code at native lifecycle points the portable model omits.
10. History, redesigns & known regrets
Lineage: Xamarin.Forms → MAUI. MAUI is the successor to Xamarin.Forms (2014). Forms wrapped native controls via renderers — heavy [assembly:ExportRenderer]-registered classes that subclassed a base renderer per control. MAUI's flagship redesign replaced renderers with the lighter handler architecture studied above: a flat PropertyMapper/CommandMapper of static methods instead of a renderer subclass, decoupled from the Forms view hierarchy. This is the migration that made handler.PlatformView and the per-property MapXxx model the core abstraction (see Handlers docs).
The declined Linux desktop target — the biggest regret. Xamarin.Forms had a community GTK backend, and a Linux/GTK target was repeatedly requested for MAUI. Microsoft declined to ship or own it. The long-running discussion dotnet/maui#339 "First class Linux support developed by Microsoft" (700+ replies) records the position: Microsoft invests in platforms "relevant to most of our current customers," scoping MAUI to mobile (iOS, Android) + desktop (Windows, macOS), with the expectation that Linux support would be community-led rather than first-party. The newer issue dotnet/maui#32023 "Official Support for .NET MAUI on Linux" continues to collect feedback, and an earlier workload request dotnet/maui#3564 asked for a maui-gtk workload. The outcome to date: no official Linux backend, only the community Maui.Gtk GTK4 project. The NotImplementedException in WindowHandler.Standard.cs is the codified result.
Other consequences of the wrapping stance. Because MAUI delegates the windowing layer, its bug surface is concentrated in the seams — title-bar customization, DPI conversion, scene/activity lifecycle, and the WinUI OnActivated-fires-twice quirk that MauiWinUIWindow.OnActivated works around with a comment citing microsoft-ui-xaml#7343:
// We have to track isActivated calls because WinUI will call OnActivated Twice // when maximizing a window
That single workaround is emblematic: MAUI inherits both the strengths and the unfixable quirks of the stacks it wraps.
Strengths
- Tiny windowing surface to maintain. By delegating windows, loops, input, IME, DPI, and decorations to native stacks, MAUI carries no compositor, no
xkbcommon, no DPI-awareness state machine — orders of magnitude less platform code than an owns-the-pixels toolkit. - Native look, feel, and behavior for free. Controls are the platform's controls, so platform conventions (IME, accessibility, text selection, momentum scroll) work without reimplementation.
- Clean per-property abstraction. The
PropertyMapper/handler model is small, testable, and extensible;handler.PlatformViewis a first-class, documented escape hatch. - Real multi-window on desktop via the native primitives (WinUI windows, UIKit scenes).
- First-party Windows + Apple integration (Mica, title-bar extension, scenes, Catalyst) tracking the latest OS APIs.
Weaknesses
- No Linux desktop, by policy — the most-requested feature is permanently community-only (#339).
- No raw windowing control. No frame pacing, no surface/
raw-window-handle, no pointer lock/confinement, no per-keystroke key events in core; apps must drop to the native handle for any of it. - Inconsistent attribute support. Window size/position/min-max/decorations are Windows-(+Catalyst-)only; the same
IWindowproperty silently no-ops on mobile. - Inherits every native quirk. The WinUI double-
OnActivated, Catalyst geometry timing, and Win32 min/max clamping are all worked around in MAUI rather than fixed. - Mac is Catalyst, not AppKit — so MAUI macOS apps carry UIKit-on-Mac compromises rather than native AppKit windowing.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Wrap native controls/windows instead of drawing | Native fidelity & accessibility for free; minimal platform code | Zero control over the windowing layer; every quirk inherited |
Handler + PropertyMapper (replaces Forms renderers) | Lighter, flat, testable, decoupled mapping | One-way property push; complex custom controls still need native code |
PlatformView is the literal native window | Trivial, transparent escape hatch | The abstraction is shallow; portable IWindow is the lowest common denominator |
| Dispatcher = thin shim over native main-thread queue | No bespoke loop to maintain; correct threading by construction | No timers/wakeups/external-fd integration beyond what the OS queue offers |
| Scope to Win/macOS/iOS/Android (+Tizen by Samsung) | Focus on platforms "relevant to most customers" | No Linux desktop; the Standard handler throws |
Win32 WndProc subclassing for missing window behavior | Implement min/max clamp & title-bar reaction WinUI won't | Fragile P/Invoke + marshalling; Windows-only |
| Logical (DIP) coordinates, density applied per platform | Single portable unit across very different scale models | Must trust each native stack's DPI awareness; MAUI can't fix mis-scaling |
Verdict: what a new framework should steal / avoid
Steal: the handler/PropertyMapper seam — making the native object directly reachable as PlatformView is a clean, honest escape hatch that admits the abstraction's limits instead of hiding them. The per-platform Dispatcher-over-native-queue shim is the right minimal loop primitive when you do not own the loop. And the conditional-compilation Mapper that simply omits unsupported properties per platform is a pragmatic way to express partial capability.
Avoid: treating "size/position/decorations" as portable when they only work on two of four platforms — silent no-ops are a worse contract than an explicit "unsupported" signal (contrast the per-dimension honesty this catalog asks of itself). And note the cost of not owning the windowing layer: a project that wants Wayland, frame pacing, pointer lock, or a custom title bar on every OS cannot get there by wrapping — that is precisely the niche an owns-the-pixels toolkit (Avalonia, Flutter, winit-based stacks) fills.
Open questions I could not resolve (with where the answer likely lives)
- Exact frame-pacing behavior on each platform under MAUI's
GraphicsView/SkiaSharp drawing path. MAUI's own drawing surfaces (PlatformTouchGraphicsView) sit on native invalidation; the cadence is set by WinUI composition / Core Animation. Likely answer:src/Graphics+ the SkiaSharp views, plus native compositor docs. - Whether any portable per-keystroke key-event API is planned. Core only exposes
KeyboardAccelerator. Likely answer:dotnet/mauidiscussions taggedarea-keyboard/proposal. - The precise lifetime/ownership of
MauiContextwindow scopes across scene reconnection on iOS.MakeWindowScope+ the scene delegate'sDidDisconnecthint at it; the definitive answer is insrc/Core/src/MauiContext*and the DI scope code (outside the sparse paths studied). - Tizen NUI windowing depth (Samsung-maintained) — coverage of decorations/DPI there was not studied in source beyond
WindowHandler.Tizen.cs.
Sources
- dotnet/maui — main repository (source for all quoted file paths) @
c389325 WindowHandler.cs/WindowHandler.Windows.cs/WindowHandler.iOS.cs/WindowHandler.Android.cs/WindowHandler.Standard.cs/WindowHandler.Tizen.cs— per-platform window handlersElementHandler.cs/ElementHandlerOfT.cs/IElementHandler.cs— the handler architectureMauiWinUIWindow.cs/MauiWinUIApplication.cs/WindowExtensions.cs/ApplicationExtensions.cs/WindowMessageManager.windows.cs— Windows/WinUI 3 windowingMauiUIApplicationDelegate.cs/MauiUISceneDelegate.cs/ApplicationExtensions.cs— iOS/Catalyst lifecycle & multi-windowDispatcher.iOS.cs/Dispatcher.Windows.cs/Dispatcher.Android.cs/Dispatcher.Standard.cs— the only loop primitiveKeyboardExtensions.cs(iOS) /KeyboardExtensions.cs(Windows) /KeyboardAcceleratorExtensions.cs/GesturePlatformManager.Windows.cs— inputClipboard.windows.cs/IClipboard— clipboard- Supported platforms / Handlers — official docs
- dotnet/maui#339 / #32023 / #3564 / microsoft-ui-xaml#7343 — history & regrets
- Maui.Gtk / Phoronix coverage — community Linux/GTK4 backend
- Sibling docs: concepts, ui-layout, async-io, winit