Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Trimming] Fix event trigger trimming warnings #20810

Merged
merged 3 commits into from
Feb 28, 2024

Conversation

simonrozsival
Copy link
Member

Description of Change

This PR fixes trimming warnings related to event triggers. Consider the following example:

<Entry Text="Hello">
    <Entry.Triggers>
        <EventTrigger Event="TextChanged">
            <local:EntryTrigger />
        </EventTrigger>
    </Entry.Triggers>
</Entry>

An app with this markup will produce the following trimming warning:

/.../maui/src/Controls/src/Core/Interactivity/EventTrigger.cs(73): Trim analysis warning IL2072: Microsoft.Maui.Controls.EventTrigger.AttachHandlerTo(BindableObject): 'name' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicEvents' in call to 'System.Reflection.RuntimeReflectionExtensions.GetRuntimeEvent(Type,String)'. The return value of method 'System.Object.GetType()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. [/.../MyMauiApp/MyMauiApp.csproj::TargetFramework=net9.0-maccatalyst]

The fix will preserve all public events on BindableObjects. This is not the optimal solution and we could consider adding a generic EventTrigger<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents)] T> variant of EventTrigger to preserve events only on types used with event triggers and improve app size.

@simonrozsival simonrozsival requested a review from a team as a code owner February 23, 2024 13:36
@@ -12,7 +12,7 @@ namespace Microsoft.Maui.Controls
[ContentProperty("Actions")]
public sealed class EventTrigger : TriggerBase
{
static readonly MethodInfo s_handlerinfo = typeof(EventTrigger).GetRuntimeMethods().Single(mi => mi.Name == "OnEventTriggered" && mi.IsPublic == false);
static readonly MethodInfo s_handlerinfo = typeof(EventTrigger).GetMethod(nameof(OnEventTriggered), BindingFlags.Instance | BindingFlags.NonPublic, binder: null, types: new Type[] { typeof(object), typeof(EventArgs) }, modifiers: null);
Copy link
Member

Choose a reason for hiding this comment

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

Could this instead be:

Suggested change
static readonly MethodInfo s_handlerinfo = typeof(EventTrigger).GetMethod(nameof(OnEventTriggered), BindingFlags.Instance | BindingFlags.NonPublic, binder: null, types: new Type[] { typeof(object), typeof(EventArgs) }, modifiers: null);
static readonly MethodInfo s_handlerinfo = new EventHandler(OnEventTriggered).Method;

Then s_handlerinfo can't be null (if we had NRT warnings in this file).

Would the OnEventTriggered method still be preserved?

Copy link
Member Author

Choose a reason for hiding this comment

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

That looks interesting, I haven't thought of that. I'll give it a try.

Copy link
Member Author

Choose a reason for hiding this comment

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

So, it turns out we cannot do exactly what you suggested because OnEventTriggered is an instance method, but I think we don't need the static s_handlerinfo at all.

@simonrozsival
Copy link
Member Author

/cc @vitek-karas

Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

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

But overall, if this solves a trimming warning, I think it is a step better than what we had before. 👍

Comment on lines -74 to +73
_handlerdelegate = s_handlerinfo.CreateDelegate(_eventinfo.EventHandlerType, this);
_handlerdelegate = ((EventHandler)OnEventTriggered).Method.CreateDelegate(_eventinfo.EventHandlerType, this);
Copy link
Member

@jonathanpeppers jonathanpeppers Feb 26, 2024

Choose a reason for hiding this comment

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

Reading this class, trying to understand why so much System.Reflection usage in the original code...

I was wondering why it is not:

EventHandler _handlerdelegate;
//...
_handlerdelegate = OnEventTriggered;

But I guess what they are doing here is trying to make EventHandler<T> of any type work?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I had the same idea, but it didn't work with generic event handlers.

@vitek-karas
Copy link
Member

Agree with everything - not a fan of so much reflection, but this change should fix the trimming parts.
Can you please check if this produces AOT warnings? The CreateDelegate call is something I would be careful about in that context.

@simonrozsival
Copy link
Member Author

@vitek-karas I don't get any AOT warnings related to EventTrigger.

The irony in this case is that we need to use CreateDelegate to match the event args type of event, but then we completely ignore the event args value 😄 Isn't there some (reflection-free) .NET hack to subscribe to an event without receiving the event args?

@@ -71,7 +70,7 @@ void AttachHandlerTo(BindableObject bindable)
try
{
_eventinfo = bindable.GetType().GetRuntimeEvent(Event);
_handlerdelegate = s_handlerinfo.CreateDelegate(_eventinfo.EventHandlerType, this);
_handlerdelegate = ((EventHandler)OnEventTriggered).Method.CreateDelegate(_eventinfo.EventHandlerType, this);
Copy link
Member

Choose a reason for hiding this comment

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

@MichalStrehovsky I can't remember the rules around CreateDelegate, is this something which might cause problems in AOT?

Copy link
Member

Choose a reason for hiding this comment

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

This is fine for AOT. If we can invoke a method, we can also create a delegate to it with reflection.

Having to create a delegate just so we can get a MethodInfo from it is unfortunate. Looks like this would benefit from a C# methodof keyword or - since we're unlikely to get methodof - dotnet/runtime#94975.

@rmarinho rmarinho merged commit 7788bbf into dotnet:net9.0 Feb 28, 2024
45 of 47 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Mar 30, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants