Skip to content

[net11.0][XSG] Trimmable EventTrigger#33611

Open
simonrozsival wants to merge 5 commits into
net11.0from
fix/33591-eventtrigger-aot
Open

[net11.0][XSG] Trimmable EventTrigger#33611
simonrozsival wants to merge 5 commits into
net11.0from
fix/33591-eventtrigger-aot

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jan 19, 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

This PR makes EventTrigger AOT-safe and trimming-safe by introducing factory methods that use compile-time event subscription instead of reflection.

Fixes #33591

Changes

New Factory Methods

Added two new EventTrigger.Create() factory methods that allow AOT-safe event subscription:

// For events using EventHandler
EventTrigger.Create<Button>("Clicked",
    static (b, h) => b.Clicked += h,
    static (b, h) => b.Clicked -= h);

// For events using EventHandler<TEventArgs>
EventTrigger.Create<Entry, TextChangedEventArgs>("TextChanged",
    static (e, h) => e.TextChanged += h,
    static (e, h) => e.TextChanged -= h);

Source Generator Integration

The XAML source generator now emits EventTrigger.Create<T>() calls instead of new EventTrigger() when the target type can be determined at compile time:

Before (reflection-based, not AOT-safe):

var eventTrigger = new EventTrigger();
eventTrigger.Event = "Clicked";

After (AOT-safe):

var eventTrigger = EventTrigger.Create<Button>("Clicked",
    static (button, handler) => button.Clicked += handler,
    static (button, handler) => button.Clicked -= handler);

Architecture

  • Introduced strategy pattern with IEventSubscriptionStrategy interface
  • ReflectionStrategy - Used by parameterless constructor (marked with [RequiresUnreferencedCode])
  • StaticStrategy<TBindable> and StaticStrategy<TBindable, TEventArgs> - Used by factory methods (fully trimming-safe)
  • Existing EventTrigger class remains sealed and backwards compatible

Backwards Compatibility

  • Existing code using new EventTrigger() continues to work (with trimming warnings)
  • The Event property is still set for all EventTrigger instances
  • Runtime XAML inflation falls back to reflection-based EventTrigger

Known Limitations

The AOT-safe code generation only works when the source generator can determine the target type at compile time. This works for the standard usage pattern:

<Button>
    <Button.Triggers>
        <EventTrigger Event="Clicked">
            <local:MyTriggerAction />
        </EventTrigger>
    </Button.Triggers>
</Button>

Edge cases that fall back to reflection-based EventTrigger:

  • EventTrigger defined inside Style.Triggers (the parent element is Style, not the actual target type)
  • EventTrigger defined in a ResourceDictionary and applied dynamically
  • Other unusual XAML structures where the parent element chain does not lead to the target BindableObject

These edge cases will continue to work at runtime but will not be AOT-safe. The source generator emits a warning (MAUIX2015) when it cannot determine the target type and falls back to reflection.

Testing

  • ✅ 122 SourceGen unit tests pass
  • ✅ 1,791 XAML unit tests pass
  • ✅ 5,427 Controls Core unit tests pass
  • ✅ 283 Essentials unit tests pass
  • Added snapshot test to verify generated code output
  • Added unit tests for new factory methods
  • Added test verifying EventTrigger fires when event occurs

Copilot AI review requested due to automatic review settings January 19, 2026 23:14
@simonrozsival simonrozsival marked this pull request as draft January 19, 2026 23:14
@simonrozsival simonrozsival changed the title Make EventTrigger AOT-safe and trimming-safe [net11.0][XSG] Make EventTrigger AOT-safe and trimming-safe Jan 19, 2026
@simonrozsival simonrozsival added perf/app-size Application Size / Trimming (sub: perf) xsg Xaml sourceGen labels Jan 19, 2026
@simonrozsival simonrozsival force-pushed the fix/33591-eventtrigger-aot branch from 7bf12d8 to 3b691d2 Compare January 19, 2026 23:23

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 introduces AOT-safe and trimming-safe alternatives for EventTrigger by adding factory methods that use compile-time event subscription instead of reflection. The implementation uses a strategy pattern to support both the legacy reflection-based approach (for backward compatibility) and new AOT-safe static strategies.

Changes:

  • Added two EventTrigger.Create<T>() factory methods for AOT-safe event subscription (one for EventHandler and one for EventHandler<TEventArgs>)
  • Integrated source generator support to automatically emit AOT-safe code when EventTriggers are used in XAML
  • Introduced strategy pattern with IEventSubscriptionStrategy interface to support both reflection-based and static event subscription
  • Marked the parameterless constructor with [RequiresUnreferencedCode] and [RequiresDynamicCode] attributes
  • Updated all platform-specific PublicAPI.Unshipped.txt files

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/Controls/src/Core/Interactivity/EventTrigger.cs Core implementation: added factory methods, strategy pattern, and refactored event subscription logic
src/Controls/src/SourceGen/EventTriggerValueProvider.cs New source generator value provider for emitting AOT-safe EventTrigger.Create calls
src/Controls/src/SourceGen/Visitors/CreateValuesVisitor.cs Integration point for EventTriggerValueProvider in the visitor pattern
src/Controls/src/SourceGen/NodeSGExtensions.cs Registered EventTriggerValueProvider as a known markup value provider
src/Controls/tests/Xaml.UnitTests/Issues/Maui33591.xaml XAML test file with EventTriggers on Button, Entry, and Switch
src/Controls/tests/Xaml.UnitTests/Issues/Maui33591.xaml.cs Tests for XAML inflation, control creation, and EventTrigger property validation
src/Controls/tests/SourceGen.UnitTests/Maui33591SourceGenTests.cs Snapshot test verifying source generator produces correct AOT-safe code
src/Controls/tests/Core.UnitTests/Triggers/EventTriggerBaseTests.cs Unit tests for factory methods, event binding, and lifecycle management
src/Controls/src/Core/PublicAPI/*/PublicAPI.Unshipped.txt Public API surface additions for all platforms (7 files)

Comment on lines +63 to +66
var eventSymbols = targetType.GetAllEvents(eventName, context).ToList();
if (eventSymbols.Count > 0)
{
var eventSymbol = eventSymbols.First();

Copilot AI Jan 19, 2026

Copy link

Choose a reason for hiding this comment

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

The event lookup logic only checks if eventSymbols.Count is greater than 0 but always uses the first event found. If multiple events with the same name exist (e.g., through inheritance or explicit interface implementation), this could select the wrong event. Consider adding validation or documentation about this behavior, or use a more specific selection strategy.

Copilot uses AI. Check for mistakes.
Comment thread src/Controls/tests/Xaml.UnitTests/Issues/Maui33591.xaml.cs Outdated
Comment thread src/Controls/src/Core/Interactivity/EventTrigger.cs
Comment thread src/Controls/src/Core/Interactivity/EventTrigger.cs Outdated
Comment thread src/Controls/src/Core/Interactivity/EventTrigger.cs Outdated
Comment on lines +112 to +130
private static ITypeSymbol? FindTargetType(ElementNode eventTriggerNode, SourceGenContext context)
{
INode? current = eventTriggerNode;

while (current != null)
{
current = current.Parent;

// Skip ListNodes (they're the collection wrapper)
if (current is ListNode listNode)
current = listNode.Parent;

// Found an ElementNode - this should be our target
if (current is ElementNode parentElement && parentElement != eventTriggerNode)
return parentElement.XmlType.GetTypeSymbol(context);
}

return null;
}

Copilot AI Jan 19, 2026

Copy link

Choose a reason for hiding this comment

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

The FindTargetType method could return null if the EventTrigger is not properly nested in the XAML tree. When this happens, the fallback code at line 96-105 is used, but there's no logging or diagnostic information to help developers understand why the AOT-safe path wasn't taken. Consider adding a diagnostic message when targetType is null to aid in debugging XAML structure issues.

Copilot uses AI. Check for mistakes.
@simonrozsival simonrozsival changed the title [net11.0][XSG] Make EventTrigger AOT-safe and trimming-safe [net11.0][XSG] Trimmable EventTrigger Jan 20, 2026
@simonrozsival simonrozsival force-pushed the fix/33591-eventtrigger-aot branch from 77bc8e8 to ec764d4 Compare January 20, 2026 13:32
@simonrozsival simonrozsival marked this pull request as ready for review January 20, 2026 13:35
@rmarinho

Copy link
Copy Markdown
Member

/rebase

@simonrozsival

Copy link
Copy Markdown
Member Author

@copilot this PR needs a proper rebase with conflict resolution. open a PR which addresses this issue. make sure it is up to date with the base net11.0 branch.

Copilot AI commented Feb 12, 2026

Copy link
Copy Markdown
Contributor

@simonrozsival I've opened a new pull request, #34012, to work on those changes. Once the pull request is ready, I'll request review from you.

@simonrozsival simonrozsival force-pushed the fix/33591-eventtrigger-aot branch from f07cb44 to 6afe2be Compare February 13, 2026 08:48
simonrozsival added a commit that referenced this pull request Feb 18, 2026
…hanges

These changes belong in separate PRs:
- #33611 (CSS StyleSheet trimming)
- #33561 (EventTrigger trimming)
- #33160 (HybridWebView trimming)

Reverted files to their main branch state, keeping only the
[ElementHandler] attribute addition on HybridWebView.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Feb 27, 2026

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 -- 33611

Or

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

@StephaneDelcroix StephaneDelcroix force-pushed the fix/33591-eventtrigger-aot branch 2 times, most recently from 0ed7c65 to bbea0c2 Compare February 27, 2026 20:24
@simonrozsival simonrozsival force-pushed the fix/33591-eventtrigger-aot branch from bbea0c2 to dfd63be Compare March 11, 2026 09:56
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run maui-pr-uitests,maui-pr-devicetests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@simonrozsival

Copy link
Copy Markdown
Member Author

@StephaneDelcroix @jfversluis could you please have a look at this PR and let me know what you think about it? could we merge this into net11.0 soon?

simonrozsival and others added 5 commits March 30, 2026 10:59
Squashed for clean rebase.
Duplicate was introduced during rebase conflict resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the source generator cannot determine the target type for an
EventTrigger (e.g., when used inside Style.Triggers or a
ResourceDictionary), it falls back to reflection-based EventTrigger.
This new warning (MAUIX2016) informs developers that the trigger
won't be AOT-safe and suggests ensuring the EventTrigger is nested
directly inside an element's Triggers collection.

The event lookup using .First() is consistent with the existing
ConnectEvent pattern in SetPropertyHelpers.cs and matches the
runtime GetRuntimeEvent behavior (most-derived event wins).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Document why .First() is correct for event lookup (most-derived wins,
  matching GetRuntimeEvent behavior and ConnectEvent in SetPropertyHelpers)
- Add Maui33591_ShadowedEvent test: DerivedButton shadows Button.Clicked
  with a new event, verifies EventTrigger subscribes to the derived one

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Leftover entries without the eventName parameter from an earlier
iteration of the API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run maui-pr-devicetests, maui-pr-uitests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@kubaflo

kubaflo commented May 24, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

@MauiBot

MauiBot commented May 24, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Merge Conflict Detected — This PR has merge conflicts with its target branch. Please rebase onto the target branch and resolve the conflicts.

@kubaflo

kubaflo commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

AI code review for net11.0 target

Verdict: Needs changes (red CI on consumer builds points to a likely PR-caused issue)

Independent review (diff-first, then reconciled with the PR narrative). This is not an approval — a human still needs to sign off.

What the PR does

Makes EventTrigger trimming/AOT-safe by introducing EventTrigger.Create<TBindable>(…) / Create<TBindable,TEventArgs>(…) factories backed by an IEventSubscriptionStrategy (static-lambda strategy vs. reflection strategy), annotating the reflection path with [RequiresUnreferencedCode]/[RequiresDynamicCode], and adding an XAML source generator (EventTriggerValueProvider) that emits the Create<…> call instead of reflection.

Findings

  • MAUIX2016 EventTriggerEventNotFound is DiagnosticSeverity.Error (build-breaking), but target-type resolution is heuristic. FindTargetType walks to the nearest parent ElementNode. For the very common Style.Triggers pattern (<Style TargetType="Entry"><Style.Triggers><EventTrigger Event="TextChanged"/>), the nearest element is the Style, which has no such event → the generator reports a build error. The same applies to ControlTemplate/DataTemplate-hosted triggers. This is the most likely explanation for the failing Build Sample App (CoreCLR/Material3/Windows) and Build Device Tests legs. Tests only cover direct Element.Triggers, not Style.Triggers. Recommend: resolve the target via Style.TargetType for style-hosted triggers, and consider downgrading to a warning + reflection fallback rather than a hard error.
  • Generated code assumes EventHandler/EventHandler<T> delegates. It emits target.{Event} += handler where handler is EventHandler/EventHandler<TArgs> and calls Create<T,TArgs> (constrained where TArgs : EventArgs). Events declared with a custom delegate type (2 params, but not EventHandler<T>, or args not derived from EventArgs) will produce non-compiling generated code or violate the generic constraint. Suggest detecting non-EventHandler<> delegates and falling back to the reflection path.
  • Public-API surface change: adding [RequiresDynamicCode]/[RequiresUnreferencedCode] to the existing public parameterless EventTrigger() ctor will surface new trim/AOT warnings in existing user code. Intentional, but call it out in the PR description as a behavioral/source change.
  • Detach correctness in StaticStrategy: removeHandler(target, InvokeActions) builds a new delegate, but it’s Equals-equal to the subscribed one (same target+method), so -= unsubscribes correctly. ✔️

CI

maui-pr (incl. AOT macOS), maui-pr-devicetests (iOS/Android/MacCatalyst/Windows build), and maui-pr-uitests (all sample-app builds) are red. Trivial net11 PRs reviewed in this batch are fully green, so these are not generic infra failures — they correlate with consumer compilation, consistent with finding #1/#2. Please confirm against the build logs.

Confidence: medium-high that the red consumer builds are PR-caused (generator error/emit); please verify the exact diagnostic in the failing Build Sample App log.

@kubaflo kubaflo 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.

PR #33611 — [net11.0][XSG] Trimmable EventTrigger (issue #33591)

Verdict: NEEDS_CHANGES (confidence: high). Great direction — making EventTrigger AOT/trim-safe by resolving the event at compile time and emitting a strongly-typed EventTrigger.Create<…> subscription (no reflection), with new MAUIX2016/2017 diagnostics and shadowed-event test coverage. But two generator edge cases break compilation/builds for valid XAML — found with strong agreement across the 3 models that completed (gpt-5.5 + gemini NEEDS_CHANGES, opus-4.6 NEEDS_DISCUSSION) and confirmed in code.

Blocking (inline)

  1. ❌ Custom-delegate events → uncompilable generated code (line 84-93). The path is chosen purely on whether the second event param is EventArgs, not on whether the event's delegate is actually EventHandler/EventHandler<T>. An event using a custom (object, TArgs) delegate emits target.Event += EventHandler<TArgs>, which doesn't convert to the custom delegate type → CS error in generated code. Only take the strongly-typed factory path for real EventHandler/EventHandler<T> events; otherwise fall back to the runtime EventTrigger.
  2. EventTrigger inside <Style> → build-breaking MAUIX2016 error (line 157). FindTargetType resolves the parent as Style (not the TargetType), the event isn't found, and MAUIX2016 is Error severity — so valid style-trigger XAML that works at runtime now fails the build (even though the runtime fallback is still emitted). Special-case Style to use its TargetType, or downgrade MAUIX2016 to a warning since a working fallback is always generated.

Strengths

The shadowed/new event handling is correct (most-derived-first member walk, matching GetRuntimeEvent), the diagnostics are wired with AnalyzerReleases.Unshipped.md + resx, and the EventHandler/EventHandler common path is clean. Both issues above are localized to the target-type/event-type resolution and are straightforward to gate.

CI

Not the blocker here — the red legs are the usual unrelated infra flakes; the generator-correctness issues above are. Happy to re-review once the two edge cases fall back to the runtime path instead of emitting broken code / a hard error.

(Note: opus-4.8 timed out without producing output this run; synthesis is based on the other 3 models + direct code verification.)

if (isGenericEventHandler)
{
var eventArgsTypeName = eventArgsType.ToFQDisplayString();
writer.WriteLine($"global::Microsoft.Maui.Controls.EventTrigger.Create<{targetTypeName}, {eventArgsTypeName}>(\"{eventName}\", static (target, handler) => target.{eventName} += handler, static (target, handler) => target.{eventName} -= handler);");

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.

Custom-delegate events generate uncompilable code. The decision at line 84-86 sets isGenericEventHandler = !eventArgsType.Equals(EventArgs) — it only inspects the event's second parameter type, never whether the event's delegate type is actually System.EventHandler / System.EventHandler<T>. So for an event declared with a custom delegate that happens to have the (object, TArgs) shape — e.g. public delegate void MyHandler(object s, MyArgs e); event MyHandler Foo; — the generator emits:

EventTrigger.Create<Target, MyArgs>("Foo", static (target, handler) => target.Foo += handler, ...);

where handler is EventHandler<MyArgs>. But target.Foo is of type MyHandler, and MyHandler += EventHandler<MyArgs> has no implicit conversion → the generated code doesn't compile (the same applies to the non-generic branch at line 97 for custom (object, EventArgs) delegates). The runtime EventTrigger handles these via Delegate.CreateDelegate; the generator should only take the strongly-typed factory path when eventSymbol.Type is exactly System.EventHandler or System.EventHandler<T>, and otherwise fall back to the runtime new EventTrigger { Event = ... } (as it already does when the target type can't be resolved). (3-model consensus: gpt-5.5, gemini, opus-4.6 — confirmed in code.)

current = listNode.Parent;

// Found an ElementNode - this should be our target
if (current is ElementNode parentElement && parentElement != eventTriggerNode)

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.

EventTrigger inside a <Style> now produces a build-breaking error for valid XAML. FindTargetType returns the parent ElementNode's type (line 157-158). For a style trigger:

<Style TargetType="Button"><Style.Triggers><EventTrigger Event="Clicked" .../></Style.Triggers></Style>

the parent element is <Style>, so targetType resolves to Microsoft.Maui.Controls.Style. Clicked doesn't exist on Style, so GetAllEvents returns empty and the code calls ReportEventNotFoundMAUIX2016, which is DiagnosticSeverity.Error (Descriptors.cs:341). The runtime fallback new EventTrigger { Event = "Clicked" } is still emitted (line 122) and would work at runtime — but because MAUIX2016 is an error, the build fails for XAML that compiles and runs today. Please either special-case Style parents to resolve against their TargetType, or — since a working runtime fallback is already emitted whenever the target/event can't be resolved — make MAUIX2016 a warning (like the MAUIX2017 target-not-resolved case at line 349) rather than an error. (3-model consensus; severity confirmed in Descriptors.cs.)

@kubaflo kubaflo mentioned this pull request Jun 16, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perf/app-size Application Size / Trimming (sub: perf) xsg Xaml sourceGen

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants