Skip to content

Alert/Dialog system (DisplayAlert, DisplayActionSheet, DisplayPromptAsync) needs a public extensibility point #34104

Description

@Redth

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    s/triagedIssue has been reviewed

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions