Skip to content

[net11.0] Make LongPressGestureRecognizer dispatch API public for platform backends - #37861

Open
Redth wants to merge 1 commit into
net11.0from
redth-long-press-gesture-dispatch
Open

[net11.0] Make LongPressGestureRecognizer dispatch API public for platform backends#37861
Redth wants to merge 1 commit into
net11.0from
redth-long-press-gesture-dispatch

Conversation

@Redth

@Redth Redth commented Aug 26, 2026

Copy link
Copy Markdown
Member

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

External platform backends can already raise tap and pointer gestures, because #37420 / #37671 made TapGestureRecognizer.SendTapped and the PointerGestureRecognizer.Send* methods public infrastructure APIs. LongPressGestureRecognizer.SendLongPressing / SendLongPressed were left internal, so an out-of-tree backend can detect a long press but has no supported way to dispatch it.

Concrete blocker: Redth/Maui.Tizen#9 has to no-op its long press dispatch.

This PR promotes both methods to public with [EditorBrowsable(EditorBrowsableState.Never)] and an ArgumentNullException guard on sender — the smallest additive surface, patterned exactly on the existing tap/pointer dispatch APIs:

[EditorBrowsable(EditorBrowsableState.Never)]
public void SendLongPressed(View sender, Func<IElement?, Point?>? getPosition = null);

[EditorBrowsable(EditorBrowsableState.Never)]
public void SendLongPressing(View sender, GestureStatus status, Func<IElement?, Point?>? getPosition = null);

A public ILongPressGestureController was considered and rejected: no other recognizer added in this wave uses a controller interface, so an interface would be inconsistent with SendTapped / SendPointer* and would add a second dispatch shape for backend authors to learn.

Nothing else changes. Command execution (including CanExecute), event raising, GestureStatus Started/Running/Completed/Canceled transitions, State updates, the lazy getPosition callback contract, touch-count filtering, cancellation semantics, threading expectations, and every in-box platform call site (iOS/MacCatalyst, Android, Windows, Tizen) are untouched. Widening accessibility has no trimming impact, and LongPressGestureRecognizer is entirely unshipped API in net11.0, so this is purely additive.

Reproducing the failure

Added src/Controls/tests/ExternalGestureBackend, a test-support assembly that is deliberately excluded from Microsoft.Maui.Controls' InternalsVisibleTo list. Before the fix it fails to build:

error CS1061: 'LongPressGestureRecognizer' does not contain a definition for 'SendLongPressing' ...
error CS1061: 'LongPressGestureRecognizer' does not contain a definition for 'SendLongPressed' ...

It now compiles, and its FakeLongPressGestureBackend drives the recognizer purely through the public API. Demoting either method back to internal breaks that project's build — the contract is enforced by the build, not just by convention.

Tests

New LongPressGestureRecognizerExternalBackendTests (Controls.Core.UnitTests) drives the fake external backend:

  • Full StartedRunningCompleted lifecycle, and LongPressed ordering before Completed
  • Canceled raises no LongPressed and executes no command
  • Cancellation when movement exceeds AllowableMovement
  • Command dispatch with CommandParameter, and CanExecute == false suppression
  • Sender identity + getPosition relative-to-element behavior
  • NumberOfTouchesRequired filtering, and fan-out to every matching recognizer on the view
  • Reflection guard asserting both methods are public and [EditorBrowsable(Never)]

Also added SendLongPressed_ThrowsForNullSender / SendLongPressing_ThrowsForNullSender to the existing LongPressGestureRecognizerTests, matching TapGestureRecognizerTests.SendTappedThrowsForNullSender.

Results: Controls.Core.UnitTests — 6219 passed, 0 failed, 30 skipped (includes the pre-existing LongPressGestureRecognizerTests and LongPressGestureRecognizerPerformanceTests). dotnet build src/Controls/src/Core/Controls.Core.csproj -p:PublicApiType=Validate -f net11.0 succeeds with 0 warnings; removing an entry from PublicAPI.Unshipped.txt correctly fails with RS0016, confirming the analyzer is enforcing the new signatures.

No device tests exist for LongPressGestureRecognizer; platform behavior stays covered by the existing LongPressGestureInteraction UI test, which is unaffected.

Docs

Added docs/design/GestureDispatchForPlatformBackends.md for backend authors: the full dispatch table across all recognizers, the shared rules (non-null sender, UI thread, lazy getPosition semantics, honoring recognizer configuration), the exact long press call ordering used by each in-box platform, and a complete example backend.

PublicAPI

Added the two entries to all seven src/Controls/src/Core/PublicAPI/* TFM folders (net, net-android, net-ios, net-maccatalyst, net-tizen, net-windows, netstandard).

Issues Fixed

Unblocks external platform backends (e.g. Redth/Maui.Tizen#9) from dispatching long press gestures.

…ends

External platform backends (for example Maui.Tizen) can raise tap and pointer
gestures because TapGestureRecognizer.SendTapped and PointerGestureRecognizer.Send*
are public infrastructure APIs. LongPressGestureRecognizer.SendLongPressing and
SendLongPressed were still internal, so an out-of-tree backend could detect a long
press but had no supported way to dispatch it.

Promote both methods to public with [EditorBrowsable(EditorBrowsableState.Never)]
and an ArgumentNullException guard on sender, matching the existing tap/pointer
dispatch APIs exactly. Command execution, event raising, GestureStatus transitions,
State updates, position callback behavior, and all in-box platform call sites are
unchanged.

Adds src/Controls/tests/ExternalGestureBackend, a test-support assembly that is
deliberately excluded from Microsoft.Maui.Controls' InternalsVisibleTo list, so its
fake backend can only compile against the public surface. Demoting either method
back to internal breaks that project's build.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 26, 2026 23:06
@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 23:06 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 37861

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 37861"

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 23:09 — with GitHub Actions Inactive
@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 23:10 — with GitHub Actions Inactive
@github-actions github-actions Bot added the area-gestures Gesture types label Aug 26, 2026
@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 23:11 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR exposes LongPressGestureRecognizer’s dispatch hooks as public infrastructure APIs so out-of-tree platform backends can raise long press gestures without InternalsVisibleTo, matching the existing tap/pointer dispatch pattern in Microsoft.Maui.Controls.

Changes:

  • Made LongPressGestureRecognizer.SendLongPressed / SendLongPressing public with [EditorBrowsable(EditorBrowsableState.Never)] and a null sender guard.
  • Added an “external backend” test-support project plus new unit tests to enforce the public dispatch surface and validate lifecycle/command semantics.
  • Updated Controls.Core PublicAPI baselines and added a design doc describing the dispatch contract for backend authors.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Controls/src/Core/LongPressGestureRecognizer.cs Promotes long-press dispatch methods to public infrastructure APIs with EditorBrowsable(Never) and ArgumentNullException guard.
src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt Adds PublicAPI entries for the two newly-public dispatch methods (netstandard).
src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt Adds PublicAPI entries for the two newly-public dispatch methods (net).
src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt Adds PublicAPI entries for the two newly-public dispatch methods (Android).
src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt Adds PublicAPI entries for the two newly-public dispatch methods (iOS).
src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt Adds PublicAPI entries for the two newly-public dispatch methods (MacCatalyst).
src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt Adds PublicAPI entries for the two newly-public dispatch methods (Tizen).
src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt Adds PublicAPI entries for the two newly-public dispatch methods (Windows).
src/Controls/tests/ExternalGestureBackend/Controls.Tests.ExternalGestureBackend.csproj Introduces a separate assembly (not in Controls’ IVT set) to compile against the public dispatch surface.
src/Controls/tests/ExternalGestureBackend/FakeLongPressGestureBackend.cs Provides a fake “third-party backend” implementation that drives the recognizer via public dispatch APIs.
src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj References the new external-backend test-support project so unit tests can exercise it.
src/Controls/tests/Core.UnitTests/Gestures/LongPressGestureRecognizerTests.cs Adds null-sender contract tests and minor comment cleanup.
src/Controls/tests/Core.UnitTests/Gestures/LongPressGestureRecognizerExternalBackendTests.cs Adds end-to-end tests validating external backend dispatch, ordering, cancellation, touch-count filtering, and EditorBrowsable(Never).
docs/design/GestureDispatchForPlatformBackends.md Documents the cross-recognizer dispatch table and long-press call ordering/contract for backend authors.

@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Member Author

Reviewer notes: design rationale + safety audit

Two questions a reviewer is likely to ask, answered up front.

1. Why public Send* methods instead of an ILongPressGestureController?

The repo has two dispatch patterns, and this PR deliberately follows the newer one:

Pattern Recognizers Shape
Legacy (Xamarin.Forms-era) PanGestureRecognizer, SwipeGestureRecognizer, PinchGestureRecognizer Explicit interface impl — void IPanGestureController.SendPan(...)
Current TapGestureRecognizer, PointerGestureRecognizer, DragGestureRecognizer, DropGestureRecognizer Public Send* + [EditorBrowsable(Never)]

The legacy interfaces are explicitly implemented, so every caller must cast:

((IPanGestureController)panGesture).SendPan(view, x, y, id);

An ILongPressGestureController would inherit that ergonomic wart and give backend authors a second dispatch shape to learn, immediately adjacent to the SendTapped / SendPointer* APIs made public in #37420/#37671 that this PR is explicitly matching. LongPressGestureRecognizer is also sealed and entirely unshipped in net11.0, so there's no compatibility pressure toward an interface.

2. Does the new ArgumentNullException guard change in-box behavior?

No — it is unreachable from every in-box call site, all of which null-check the view before dispatching:

Call site Guard
Platform/Android/LongPressGestureHandler.cs if (view == null) return;
GestureManager/GesturePlatformManager.Android.cs (a11y key path) if (View is null || sender is not AView ...) return;
GestureManager/GesturePlatformManager.iOS.cs if (lpRecognizer == null || view == null) return;
GestureManager/LongPressGestureHandler.Windows.cs if (view == null) return;, non-null view captured in the timer closure
Platform/Tizen/LongPressGestureHandler.cs if (View == null) return;

Precedent: SendTapped already carries this identical guard and is called from the same Android accessibility path (GesturePlatformManager.Android.cs), so the pattern is already proven in-box. Parameter lists are unchanged, so this is accessibility-only — no call site needed touching.

3. No reflection / no internals

FakeLongPressGestureBackend in the new Controls.Tests.ExternalGestureBackend assembly contains zero reflection and zero internal access — it compiles purely against the public surface, and that assembly is deliberately absent from Controls.Core's InternalsVisibleTo list. (The one System.Reflection use is a test assertion in LongPressGestureRecognizerExternalBackendTests verifying the methods are public + EditorBrowsable(Never) — it is not part of the dispatch path.)

Caveat worth flagging

IncludeTizenTargetFrameworks is hard-disabled in CI (Directory.Build.props: "Disabled until net10.0-tizen is available"), so the in-box Platform/Tizen/LongPressGestureHandler.cs — which uses exactly this dispatch surface — and the net-tizen/PublicAPI.Unshipped.txt entries are not compile-verified by CI. That's pre-existing and not introduced here; the net-tizen PublicAPI entries were added for consistency with the other six TFM folders.

Separately: the in-box Tizen handler maps Started / Finished / Cancelled but not Tizen's ContinuingGestureStatus.Running. Filing that as an observation rather than changing uncompilable code in an API-exposure PR.

Redth added a commit to Redth/Maui.Tizen that referenced this pull request Aug 27, 2026
Merge conflicts were the solution's test folder and the workload-free lane's
project list. Core reworded its RefPackCompile commentary and split the sample
into its own lane, so core's text was taken verbatim and this branch's two
Controls entries re-added around it.

Long-press mapping
------------------
dotnet/maui#37861 makes SendLongPressed and SendLongPressing public. It is
code/CI complete but NOT merged, so it is absent from the pinned package and is
not adopted here. What is landed is the translation that adoption will need,
specified and tested now so it is a small change later rather than a fresh
translation written at the time.

The in-box Tizen handler is deliberately NOT the model. Checked against
dotnet/maui net11.0: LongPressGestureHandler.cs has no Continuing branch at all,
so a Tizen long press never reports GestureStatus.Running and an app tracking
the gesture sees Started jump straight to Completed. iOS maps its equivalent
(UIGestureRecognizerState.Changed) to Running - verified in
GesturePlatformManager.iOS.cs - and that is what this backend follows:

  Started    -> Started    LongPressing
  Continuing -> Running    LongPressing
  Finished   -> Completed  LongPressed FIRST, then LongPressing
  Cancelled  -> Canceled   LongPressing only; never LongPressed, never Command

A canceled press reports a status change but is not a press, so raising
LongPressed or running the command there would fire the app's handler for a
gesture the user aborted. CompletesLongPress encodes that and is tested per
state.

The handler previously dropped Continuing, inheriting the in-box gap. It now
passes it through, and ToLongPressStatus/CompletesLongPress encode the table
above. Reintroducing the gap fails four tests across both the mapping and the
handler's ordering, so it cannot come back silently during adoption.

208 tests, up from 196. The API15 compile lane passes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-gestures Gesture types

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants