Third-party platform backends have no supported way to implement DisplayAlert(), DisplayActionSheet(), or DisplayPromptAsync(). The alert pipeline relies on an internal AlertManager class and a private nested IAlertManagerSubscription interface, forcing custom backends to use DispatchProxy + heavy reflection to intercept dialog requests. MAUI should expose a public interface for custom alert implementations.
Problem
When a MAUI app calls page.DisplayAlert(...), the request flows through an internal pipeline:
Page.DisplayAlert()
→ AlertManager (internal class)
→ IAlertManagerSubscription (internal nested interface)
→ Platform-specific AlertRequestHelper
Every type in this chain is internal. A custom platform backend (e.g. Linux/GTK) must:
1. Discover internal types via reflection
// Get the internal AlertManager type from MAUI's assembly
var amType = typeof(Window).Assembly
.GetType("Microsoft.Maui.Controls.Platform.AlertManager");
// Get the private nested IAlertManagerSubscription interface
var iamsType = amType.GetNestedType("IAlertManagerSubscription",
BindingFlags.Public | BindingFlags.NonPublic);
2. Create a DispatchProxy at runtime to implement the internal interface
Since we can't implement an interface we can't reference, we use System.Reflection.DispatchProxy to generate a runtime proxy:
var proxyType = typeof(AlertSubscriptionProxy<>).MakeGenericType(iamsType);
var createMethod = typeof(DispatchProxy)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.First(m => m.Name == "Create" && m.GetGenericArguments().Length == 2)
.MakeGenericMethod(iamsType, proxyType);
var proxy = createMethod.Invoke(null, null);
services.AddSingleton(iamsType, proxy);
3. Parse alert arguments via reflection (no typed access)
The proxy intercepts method calls by name (OnAlertRequested, OnActionSheetRequested, OnPromptRequested) and extracts parameters from opaque event args objects:
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
switch (targetMethod.Name)
{
case "OnAlertRequested":
var title = GetProp<string>(args[1]!, "Title");
var message = GetProp<string>(args[1]!, "Message");
var result = GetProp<object>(args[1]!, "Result"); // TaskCompletionSource
// ...
}
}
// Every property access is reflection
static TResult? GetProp<TResult>(object obj, string name)
{
var prop = obj.GetType().GetProperty(name);
return prop != null ? (TResult?)prop.GetValue(obj) : default;
}
4. Complete results via reflection on TaskCompletionSource
// Can't cast to TaskCompletionSource<bool> because it comes from an internal type
var trySetResult = result?.GetType().GetMethod("TrySetResult");
trySetResult?.Invoke(result, [true]);
5. Trigger AlertManager.Subscribe() via reflection in WindowHandler
MAUI's AlertManager auto-subscribes on built-in platforms, but custom backends must manually trigger it:
var amProp = typeof(Window).GetProperty("AlertManager",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
var alertManager = amProp?.GetValue(mauiWindow);
var subscribe = alertManager?.GetType().GetMethod("Subscribe",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
subscribe?.Invoke(alertManager, null);
Why This Is Fragile
- Any internal refactor breaks it — renaming
AlertManager, IAlertManagerSubscription, or any event args property silently breaks all custom backends at runtime
- No compile-time safety — everything is string-based reflection; typos are runtime crashes
DispatchProxy overhead — generating proxy types at runtime adds startup cost and complexity
- Swallowed exceptions — the entire registration is wrapped in
try { } catch { } because there's no way to know if the internal API changed
Proposed Solution
Option A (minimal): Make IAlertManagerSubscription public
namespace Microsoft.Maui.Controls.Platform;
public interface IAlertManagerSubscription
{
void OnAlertRequested(Page sender, AlertArguments args);
void OnActionSheetRequested(Page sender, ActionSheetArguments args);
void OnPromptRequested(Page sender, PromptArguments args);
}
Also make AlertArguments, ActionSheetArguments, and PromptArguments public (or they may already be — the Result TaskCompletionSource property needs to be publicly typed).
Custom backends would then implement the interface directly:
public class GtkAlertSubscription : IAlertManagerSubscription
{
public void OnAlertRequested(Page sender, AlertArguments args)
{
// Show GTK dialog, call args.Result.TrySetResult(true/false)
}
// ...
}
// Clean DI registration — no reflection needed
services.AddSingleton<IAlertManagerSubscription, GtkAlertSubscription>();
Option B (preferred): Service-based alert provider
namespace Microsoft.Maui.Controls;
public interface IAlertDialogProvider
{
Task<bool> DisplayAlertAsync(string title, string message, string accept, string cancel);
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
Task<string?> DisplayPromptAsync(string title, string message, string accept, string cancel,
string? placeholder, string? initialValue);
}
MAUI resolves from DI; built-in platforms register their implementation; custom backends register theirs:
builder.Services.AddSingleton<IAlertDialogProvider, GtkAlertDialogProvider>();
Option C: Handler-based approach
Add alert/dialog methods to IWindowHandler or a new IDialogHandler so platform handlers naturally include dialog support:
public partial interface IWindowHandler : IElementHandler
{
Task<bool> DisplayAlertAsync(string title, string message, string accept, string cancel);
// ...
}
Real-World Reference
The full workaround implementation is in the Maui.Gtk project:
See also: https://gist.github.com/Redth/fc07a982bcff79cf925168f241a12c95
Third-party platform backends have no supported way to implement
DisplayAlert(),DisplayActionSheet(), orDisplayPromptAsync(). The alert pipeline relies on an internalAlertManagerclass and a private nestedIAlertManagerSubscriptioninterface, forcing custom backends to useDispatchProxy+ heavy reflection to intercept dialog requests. MAUI should expose a public interface for custom alert implementations.Problem
When a MAUI app calls
page.DisplayAlert(...), the request flows through an internal pipeline:Every type in this chain is internal. A custom platform backend (e.g. Linux/GTK) must:
1. Discover internal types via reflection
2. Create a
DispatchProxyat runtime to implement the internal interfaceSince we can't implement an interface we can't reference, we use
System.Reflection.DispatchProxyto generate a runtime proxy:3. Parse alert arguments via reflection (no typed access)
The proxy intercepts method calls by name (
OnAlertRequested,OnActionSheetRequested,OnPromptRequested) and extracts parameters from opaque event args objects:4. Complete results via reflection on
TaskCompletionSource5. Trigger
AlertManager.Subscribe()via reflection inWindowHandlerMAUI's
AlertManagerauto-subscribes on built-in platforms, but custom backends must manually trigger it:Why This Is Fragile
AlertManager,IAlertManagerSubscription, or any event args property silently breaks all custom backends at runtimeDispatchProxyoverhead — generating proxy types at runtime adds startup cost and complexitytry { } catch { }because there's no way to know if the internal API changedProposed Solution
Option A (minimal): Make
IAlertManagerSubscriptionpublicAlso make
AlertArguments,ActionSheetArguments, andPromptArgumentspublic (or they may already be — theResultTaskCompletionSourceproperty needs to be publicly typed).Custom backends would then implement the interface directly:
Option B (preferred): Service-based alert provider
MAUI resolves from DI; built-in platforms register their implementation; custom backends register theirs:
Option C: Handler-based approach
Add alert/dialog methods to
IWindowHandleror a newIDialogHandlerso platform handlers naturally include dialog support:Real-World Reference
The full workaround implementation is in the Maui.Gtk project:
GtkAlertManager.cs— 370 lines of reflection and DispatchProxyWindowHandler.cs— reflectiveAlertManager.Subscribe()triggerSee also: https://gist.github.com/Redth/fc07a982bcff79cf925168f241a12c95