Add delegate-based alert dialog extensibility convention (no public API changes)#35095
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35095Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35095" |
🔍 Multi-Model Code Review — PR #35095PR: Add delegate-based alert dialog extensibility convention (no public API changes) 🟡 MODERATE1. Null
2. Delegate that returns successfully without calling
3. Missing test coverage for ActionSheet and Prompt delegate paths (2/3 reviewers)
4. DI type collision risk with
🟢 MINOR5. No test for asynchronous fault propagation (3/3 reviewers)
6. No test for cancellation forwarding (3/3 reviewers)
7. Timing-dependent fall-through test assertion (2/3 reviewers)
8. Unawaited Task in
9. Fallback subscription eagerly allocated when all delegates registered (2/3 reviewers)
10. Redundant null check on
Discarded Findings
Thread Safety ✅No thread-safety issues introduced. Test Coverage AssessmentThe 4 new tests cover the happy path well for alerts, but leave significant gaps:
Recommendation:
|
|
Re: multi-model review. Thanks for the careful pass - addressed in aa42106. Moderate
Minor
Also added |
|
Re: dogfood instructions. Thanks. This is the automated dogfood instructions comment and does not request a code change, so no local changes were needed for this comment. |
|
Re: multi-model review. Thanks again. The earlier reply/commit addressed points 1-7, and I also addressed 8-10 in 2764b77: the explicit-subscription test now awaits completion, delegate fallback creation is lazy, and the redundant |
|
Follow-up for #35095 (comment): thanks, I rechecked the current code and the substantive items are addressed: null/forgotten SetResult now fail fast, ActionSheet/Prompt plus async fault/cancel tests are covered, the fall-through test is deterministic, the explicit subscription task is awaited, fallback creation is lazy, and the redundant null check is gone. For the Func<> collision concern, I kept the keyed-only convention so unkeyed registrations are ignored, and pushed 8ee9d14 to fix the remaining internal comment so it documents AddKeyedSingleton with the MAUI service keys. |
There was a problem hiding this comment.
Pull request overview
This PR adds an internal, DI-based extensibility convention to let third-party platform backends intercept DisplayAlert / DisplayActionSheet / DisplayPrompt by registering keyed delegate services, without introducing new public API surface.
Changes:
- Add keyed-service lookup in
AlertManager.Subscribe()to create an internal delegate-backed subscription when any of the dialog delegates are registered. - Introduce
DelegateAlertSubscriptionto dispatch dialog requests to delegates and enforce completion contracts (fault/cancel/invalid contract → caller observes it). - Add unit tests covering delegate dispatch, precedence, fall-through behavior, and contract violations; update argument type remarks to document the convention.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/src/Core/Platform/AlertManager/AlertManager.cs | Adds internal service keys and delegate-convention subscription creation during Subscribe(). |
| src/Controls/src/Core/Platform/AlertManager/DelegateAlertSubscription.cs | New internal subscription that forwards to keyed delegates or falls back to platform defaults. |
| src/Controls/src/Core/AlertArguments.cs | Documents keyed delegate convention for alerts. |
| src/Controls/src/Core/ActionSheetArguments.cs | Documents keyed delegate convention for action sheets. |
| src/Controls/src/Core/PromptArguments.cs | Documents keyed delegate convention for prompts. |
| src/Controls/tests/Core.UnitTests/AlertManagerTests.cs | Adds tests validating delegate convention behavior and error/cancellation propagation. |
|
/review |
|
✅ Expert Code Review completed successfully! |
There was a problem hiding this comment.
Expert Code Review — PR #35095
Methodology: 3 independent reviewers with adversarial consensus (disputed findings validated by cross-model follow-up)
Findings
| # | Severity | File | Consensus | Finding |
|---|---|---|---|---|
| 1 | 🟡 MODERATE | DelegateAlertSubscription.cs:131-134 |
2/3 reviewers | "SetResult before Task completes" contract may surprise third-party implementers. MAUI's own platform AlertRequestHelper uses fire-and-forget (show dialog → return → callback calls SetResult). The new delegate contract requires await-to-completion before returning, which differs from the internal idiom. Well-documented, but discoverability could be improved with a code sample in the XML docs. |
| 2 | 🟢 MINOR | AlertManager.cs:11-13 |
2/3 reviewers | Internal constants form an undocumented string-based contract. Third-party consumers must hardcode string keys with no compile-time validation. Intentional trade-off for zero public API — consider adding a stability comment on the constants. |
Discarded Findings (single reviewer only)
The following were flagged by only 1 of 3 reviewers and rejected during adversarial follow-up:
_fallback.Valuenull risk — all platformCreateSubscriptionimplementations always return non-null (confirmed by 2 follow-up reviewers)Lazy<T>exception caching — low practical risk sinceSubscribe()runs during valid window setupExecuteSynchronouslyonContinueWith— correct for lightweightForwardCompletion- Test coverage gaps (error tests only for
AlertArguments) — shared generic code path - Test timing assertion on
resultTask.IsCompleted— valid given test infrastructure
CI Status
✅ All 39 CI checks passed (build, unit tests, integration tests, device tests).
Test Coverage Assessment
The PR adds 11 new unit tests covering: delegate dispatch (alert, action sheet, prompt), precedence over delegate convention, unkeyed delegate rejection, per-operation fallback, synchronous/asynchronous fault propagation, cancellation forwarding, null task handling, and missing SetResult contract violation. Coverage is thorough for the new code paths.
Verdict: LGTM with suggestions
Confidence: high
The implementation is clean, well-structured, and thoroughly tested. The delegate-based extensibility convention using keyed DI services is a pragmatic solution for .NET 10 without new public API. The two suggestions above (doc sample for the SetResult contract, stability comment on string keys) are non-blocking improvements that would help third-party consumers.
Generated by Expert Code Review for issue #35095 · ● 21.6M
|
/review |
|
✅ Expert Code Review completed successfully! |
There was a problem hiding this comment.
Expert Code Review — PR #35095
Methodology: 3 independent reviewers with adversarial consensus (disputed findings validated by cross-reviewer agreement)
Findings
| # | Severity | File | Consensus | Finding |
|---|---|---|---|---|
| 1 | 🟢 MINOR | DelegateAlertSubscription.cs:95, 128-130 |
2/3 reviewers | Cancellation semantics not fully preserved. Two aspects: (a) TrySetCanceled() without args loses the original CancellationToken from the delegate's canceled task; (b) sync throw OperationCanceledException in the catch block produces a faulted task rather than a canceled one. Low practical impact today since MAUI callers don't inspect the token, but sets imprecise precedent for third-party extensibility. See inline comment for suggested fix. |
Discarded Findings (did not reach consensus)
- Duplicate
ExecuteSynchronouslyparameter — flagged by 1 reviewer from the diff, but verified on disk that the file is correct (single parameter). The GitHub API diff contained a rendering artifact. - Internal service key discoverability — flagged by 1 reviewer, but this was addressed in a prior review round (author added stability comment on the constants). Not re-flagged.
- Error-path test coverage — 1 reviewer noted error-path tests only exercise
AlertArgumentsdelegates, notActionSheetArguments/PromptArguments. Since theInvoke<T>method is generic and shared across all three, the risk of a type-specific regression is very low.
CI Status
⏳ CI checks are currently in progress (build triggered after latest commits at e720f20). Cannot confirm pass/fail at this time.
Test Coverage Assessment
The PR adds 11 new unit tests covering: delegate dispatch for all 3 dialog types, precedence over delegate convention, unkeyed delegate rejection, per-operation fallthrough, synchronous and asynchronous fault propagation, cancellation forwarding, null task contract violation, and result-only completion. Coverage is thorough for the new code paths.
Summary
The implementation is clean, well-structured, and thoroughly tested. The delegate-based extensibility convention using keyed DI services is a pragmatic solution for .NET 10 that avoids new public API surface. The single finding is a non-blocking suggestion to improve cancellation fidelity for third-party consumers. All prior review feedback has been addressed.
Generated by Expert Code Review · 3-model adversarial consensus
Warning
⚠️ Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
pkgs.dev.azure.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "pkgs.dev.azure.com"See Network Configuration for more information.
Generated by Expert Code Review for issue #35095 · ● 20.1M
Multimodal Review — PR #35095Reviewed: code, related issue (#34104), prior reviewer threads, commit history (8 commits, all addressing reviewer feedback), the alternative public‑API approach (#33267), and the surrounding Verdict: LGTM with a few low‑priority polish itemsThis is a clean, pragmatic .NET 10 unblocker for third‑party platform backends (e.g. Maui.Gtk) that today have to use I’ve verified each of the prior expert‑review fixes landed in
New observations (not yet raised)🟢
|
Third-party platform backends can now provide custom DisplayAlertAsync /
DisplayActionSheetAsync / DisplayPromptAsync implementations without MAUI
exposing any new public API. AlertManager.Subscribe() now looks up three
already-publicly-typed delegates in the service provider:
Func<Page, AlertArguments, Task>
Func<Page, ActionSheetArguments, Task>
Func<Page, PromptArguments, Task>
If any are registered, MAUI wraps them in an internal DelegateAlertSubscription
that invokes the delegate for operations that have one and falls through to the
platform-default subscription for operations that don't. This lets backends
opt in incrementally and avoid the DispatchProxy + reflection workaround
documented in #34104.
Explicitly-registered IAlertManagerSubscription (the existing internal path)
still takes precedence. Delegate exceptions and cancellations are forwarded
to the caller via the argument's TaskCompletionSource.
Adds unit test coverage for interception, fall-through, precedence, and
exception propagation. No changes to PublicAPI.*.txt.
Fixes #34104
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Surface InvalidOperationException via the TaskCompletionSource when an alert/action sheet/prompt delegate returns a null Task or completes successfully without calling SetResult, instead of silently hanging. - Use TrySetException so well-behaved delegates that did call SetResult are unaffected. - Add tests for ActionSheet and Prompt delegate paths, asynchronous fault propagation, cancellation forwarding, null-Task and missing-SetResult contract violations. - Replace the wall-clock racing fall-through test with a deterministic invocation-tracking assertion. - Document in AlertArguments / ActionSheetArguments / PromptArguments remarks that the Func<Page, *Args, Task> service key is reserved for MAUI alert handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make the delegate alert fallback subscription lazy so platform fallback objects are only created when needed. Await the explicit-subscription precedence test so failures surface through the test task, and simplify fault forwarding by removing the redundant null check after Task.IsFaulted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve alert dialog delegates only from keyed DI registrations so the convention does not consume unrelated Func services. Document the literal keys for alert, action sheet, and prompt handlers, and add coverage that unkeyed delegates are ignored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the internal delegate subscription example to show keyed service registrations so it matches the keyed-only convention used by AlertManager. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Change the keyed alert dialog delegates to return Task<TResult> so MAUI completes the AlertArguments, ActionSheetArguments, and PromptArguments internally from the returned result. This removes the surprising requirement that third-party implementations call SetResult before their delegate task completes while keeping the convention keyed-only and public-API-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
07e38b2 to
7475d22
Compare
…PI changes) (#35095) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change Enables third-party platform backends (for example Maui.Gtk) to provide their own `DisplayAlertAsync` / `DisplayActionSheetAsync` / `DisplayPromptAsync` implementations without MAUI exposing any new public API. This complements #33267: because that PR introduces a new public interface, it must wait for .NET 11. This PR solves the same problem using already-shipped public argument types, so it is shippable in .NET 10. `AlertManager.Subscribe()` now keeps the existing explicit internal `IAlertManagerSubscription` path, then checks for keyed delegate registrations before falling back to the platform default. The delegate signatures use only existing public types: ```csharp Func<Page, AlertArguments, Task<bool>> Func<Page, ActionSheetArguments, Task<string>> Func<Page, PromptArguments, Task<string>> ``` The registrations are keyed so MAUI does not accidentally consume unrelated `Func<>` services: | Dialog | Service key | |---|---| | Alert | `Microsoft.Maui.Controls.DisplayAlert` | | Action sheet | `Microsoft.Maui.Controls.DisplayActionSheet` | | Prompt | `Microsoft.Maui.Controls.DisplayPrompt` | Consumer usage: ```csharp builder.Services.AddKeyedSingleton<Func<Page, AlertArguments, Task<bool>>>( "Microsoft.Maui.Controls.DisplayAlert", async (page, args) => { return await MyGtkDialog.ShowAsync(args.Title, args.Message, args.Accept, args.Cancel); }); ``` If any keyed delegate is registered, MAUI wraps it in a new internal `DelegateAlertSubscription`. Registered operations dispatch to their delegate; unregistered operations fall through to the platform default. The delegate returns the dialog result and MAUI completes the corresponding `AlertArguments`, `ActionSheetArguments`, or `PromptArguments` internally. If the delegate returns `null`, faults, or cancels, the caller observes that through the original `Display*Async` task instead of hanging silently. Precedence order in `Subscribe()`: 1. Explicit `IAlertManagerSubscription` service (existing internal path, unchanged) 2. Keyed result-returning delegate convention (new) 3. Platform default subscription (existing fallback) ### Notes for reviewers - Zero public API surface: no `PublicAPI.*.txt` changes. The new `DelegateAlertSubscription` class and key constants are internal; consumers use literal string keys documented on the existing argument types. - Keyed registrations are intentional. Unkeyed `Func<>` services are ignored to avoid accidental collisions. - The delegate returns the dialog result instead of calling `args.SetResult(...)`. This matches the built-in async platform pattern where MAUI completes the argument after awaiting the native dialog result. - Per-operation fall-through is intentional. A backend can override just alerts and keep the platform action sheet/prompt, for example. - `OnPageBusy` is excluded from the convention (obsolete in .NET 10, removed in .NET 11) and always routes to the fallback. - When .NET 11 ships a proper public `IAlertManager`/`IAlertDialogProvider`, this delegate convention can stay as a lightweight alias or be deprecated. Either way, .NET 10 consumers are unblocked now. ### Tests Unit coverage in `AlertManagerTests.cs` includes: - Alert, action sheet, and prompt delegate dispatch - Returned delegate results completing the original `Display*Async` caller - Unregistered operation fall-through - Unkeyed delegate services being ignored - Explicit `IAlertManagerSubscription` precedence - Synchronous and asynchronous delegate faults - Delegate cancellation forwarding - Null task contract violation Focused `AlertManagerTests` pass locally: 21/21. ### Issues Fixed Fixes #34104 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Description of Change
Enables third-party platform backends (for example Maui.Gtk) to provide their own
DisplayAlertAsync/DisplayActionSheetAsync/DisplayPromptAsyncimplementations without MAUI exposing any new public API. This complements #33267: because that PR introduces a new public interface, it must wait for .NET 11. This PR solves the same problem using already-shipped public argument types, so it is shippable in .NET 10.AlertManager.Subscribe()now keeps the existing explicit internalIAlertManagerSubscriptionpath, then checks for keyed delegate registrations before falling back to the platform default. The delegate signatures use only existing public types:The registrations are keyed so MAUI does not accidentally consume unrelated
Func<>services:Microsoft.Maui.Controls.DisplayAlertMicrosoft.Maui.Controls.DisplayActionSheetMicrosoft.Maui.Controls.DisplayPromptConsumer usage:
If any keyed delegate is registered, MAUI wraps it in a new internal
DelegateAlertSubscription. Registered operations dispatch to their delegate; unregistered operations fall through to the platform default. The delegate returns the dialog result and MAUI completes the correspondingAlertArguments,ActionSheetArguments, orPromptArgumentsinternally. If the delegate returnsnull, faults, or cancels, the caller observes that through the originalDisplay*Asynctask instead of hanging silently.Precedence order in
Subscribe():IAlertManagerSubscriptionservice (existing internal path, unchanged)Notes for reviewers
PublicAPI.*.txtchanges. The newDelegateAlertSubscriptionclass and key constants are internal; consumers use literal string keys documented on the existing argument types.Func<>services are ignored to avoid accidental collisions.args.SetResult(...). This matches the built-in async platform pattern where MAUI completes the argument after awaiting the native dialog result.OnPageBusyis excluded from the convention (obsolete in .NET 10, removed in .NET 11) and always routes to the fallback.IAlertManager/IAlertDialogProvider, this delegate convention can stay as a lightweight alias or be deprecated. Either way, .NET 10 consumers are unblocked now.Tests
Unit coverage in
AlertManagerTests.csincludes:Display*AsynccallerIAlertManagerSubscriptionprecedenceFocused
AlertManagerTestspass locally: 21/21.Issues Fixed
Fixes #34104