Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 176 additions & 27 deletions src/Controls/src/Core/Interactivity/EventTrigger.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Internals;
Expand All @@ -14,26 +14,90 @@ 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);
readonly List<WeakReference<BindableObject>> _associatedObjects = new List<WeakReference<BindableObject>>();

EventInfo _eventinfo;

readonly IEventSubscriptionStrategy _strategy;
string _eventname;
Delegate _handlerdelegate;

/// <summary>
/// Initializes a new <see cref="EventTrigger" /> instance.
/// </summary>
public EventTrigger() : base(typeof(BindableObject))
[RequiresUnreferencedCode("EventTrigger uses reflection to subscribe to events. Use EventTrigger.Create<T>() factory methods for trimming-safe alternatives.")]
#if !NETSTANDARD
[RequiresDynamicCode("EventTrigger uses reflection to subscribe to events. Use EventTrigger.Create<T>() factory methods for trimming-safe alternatives.")]
#endif
public EventTrigger()
: base(typeof(BindableObject))
{
_strategy = new ReflectionStrategy(this);
}

private EventTrigger(Func<EventTrigger, IEventSubscriptionStrategy> strategyFactory)
: base(typeof(BindableObject))
{
_strategy = strategyFactory(this);
}

#nullable enable
/// <summary>
/// Creates an AOT-safe EventTrigger for events using <see cref="EventHandler"/>.
/// </summary>
/// <typeparam name="TBindable">The type of the bindable object that owns the event.</typeparam>
/// <param name="eventName">The name of the event to respond to.</param>
/// <param name="addHandler">A static lambda to subscribe to the event.</param>
/// <param name="removeHandler">A static lambda to unsubscribe from the event.</param>
/// <returns>A new EventTrigger instance.</returns>
/// <example>
/// <code>
/// EventTrigger.Create&lt;Button&gt;("Clicked",
/// static (b, h) =&gt; b.Clicked += h,
/// static (b, h) =&gt; b.Clicked -= h);
/// </code>
/// </example>
public static EventTrigger Create<TBindable>(
string eventName,
Action<TBindable, EventHandler> addHandler,
Action<TBindable, EventHandler> removeHandler) where TBindable : BindableObject
{
return new EventTrigger(trigger => new StaticStrategy<TBindable>(addHandler, removeHandler, trigger))
{
Event = eventName
};
}

/// <summary>
/// Creates an AOT-safe EventTrigger for events using <see cref="EventHandler{TEventArgs}"/>.
/// </summary>
/// <typeparam name="TBindable">The type of the bindable object that owns the event.</typeparam>
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
/// <param name="eventName">The name of the event to respond to.</param>
/// <param name="addHandler">A static lambda to subscribe to the event.</param>
/// <param name="removeHandler">A static lambda to unsubscribe from the event.</param>
/// <returns>A new EventTrigger instance.</returns>
/// <example>
/// <code>
/// EventTrigger.Create&lt;Entry, TextChangedEventArgs&gt;("TextChanged",
/// static (e, h) =&gt; e.TextChanged += h,
/// static (e, h) =&gt; e.TextChanged -= h);
/// </code>
/// </example>
public static EventTrigger Create<TBindable, TEventArgs>(
string eventName,
Action<TBindable, EventHandler<TEventArgs>> addHandler,
Action<TBindable, EventHandler<TEventArgs>> removeHandler)
where TBindable : BindableObject
where TEventArgs : EventArgs
{
Actions = new SealedList<TriggerAction>();
return new EventTrigger(trigger => new StaticStrategy<TBindable, TEventArgs>(addHandler, removeHandler, trigger))
{
Event = eventName
};
}
#nullable disable

/// <summary>
/// Gets the collection of <see cref="TriggerAction" /> objects to invoke when the event fires.
/// </summary>
public IList<TriggerAction> Actions { get; }
public IList<TriggerAction> Actions { get; } = new SealedList<TriggerAction>();

/// <summary>
/// Gets or sets the name of the event that triggers the actions.
Expand All @@ -56,8 +120,7 @@ public string Event
internal override void OnAttachedTo(BindableObject bindable)
{
base.OnAttachedTo(bindable);
if (!string.IsNullOrEmpty(Event))
AttachHandlerTo(bindable);
_strategy.AttachHandlerTo(bindable);
_associatedObjects.Add(new WeakReference<BindableObject>(bindable));
}

Expand All @@ -67,7 +130,7 @@ internal override void OnDetachingFrom(BindableObject bindable)
{
if (wr.TryGetTarget(out var target) && target == bindable)
{
DetachHandlerFrom(bindable);
_strategy.DetachHandlerFrom(bindable);
return true;
}
return false;
Expand All @@ -81,32 +144,118 @@ internal override void OnSeal()
((SealedList<TriggerAction>)Actions).IsReadOnly = true;
}

void AttachHandlerTo(BindableObject bindable)
private void InvokeActions(object sender)
{
if (sender is not BindableObject bindable)
return;

foreach (TriggerAction action in Actions)
action.DoInvoke(bindable);
}

#nullable enable
private interface IEventSubscriptionStrategy
{
try
void AttachHandlerTo(BindableObject target);
void DetachHandlerFrom(BindableObject target);
}

[RequiresUnreferencedCode("Uses reflection to subscribe to events.")]
#if !NETSTANDARD
[RequiresDynamicCode("Uses reflection to subscribe to events.")]
#endif
private sealed class ReflectionStrategy(EventTrigger trigger) : IEventSubscriptionStrategy
{
private EventInfo? _eventInfo;
private Delegate? _handler;

public void AttachHandlerTo(BindableObject target)
{
_eventinfo = bindable.GetType().GetRuntimeEvent(Event);
_handlerdelegate = ((EventHandler)OnEventTriggered).Method.CreateDelegate(_eventinfo.EventHandlerType, this);
if (string.IsNullOrEmpty(trigger.Event))
return;

try
{
_eventInfo = target.GetType().GetRuntimeEvent(trigger.Event);
if (_eventInfo == null)
{
LogWarning(target);
return;
}

// Create a delegate that matches the event's signature
var handlerMethod = typeof(ReflectionStrategy).GetMethod(nameof(OnEventTriggered), BindingFlags.NonPublic | BindingFlags.Instance);
_handler = handlerMethod!.CreateDelegate(_eventInfo.EventHandlerType!, this);
_eventInfo.AddEventHandler(target, _handler);
}
catch (Exception)
{
LogWarning(target);
}
}
catch (Exception)

public void DetachHandlerFrom(BindableObject target)
{
if (_eventInfo != null && _handler != null)
_eventInfo.RemoveEventHandler(target, _handler);
}

private void OnEventTriggered(object? sender, EventArgs e)
{
trigger.InvokeActions(sender);
}

private void LogWarning(BindableObject target)
{
MauiLogger<EventTrigger>.Log(LogLevel.Warning, $"Cannot attach EventTrigger to {bindable.GetType()}.{Event}. Check if the handler exists and if the signature is right.");
Application.Current?.FindMauiContext()?.CreateLogger<EventTrigger>()?
.LogWarning("Cannot attach EventTrigger to {Type}.{Event}. Check if the event exists and the signature is correct.",
target.GetType(), trigger.Event);
}
if (_eventinfo != null && _handlerdelegate != null)
_eventinfo.AddEventHandler(bindable, _handlerdelegate);
}

void DetachHandlerFrom(BindableObject bindable)
private sealed class StaticStrategy<TBindable>(
Action<TBindable, EventHandler> addHandler,
Action<TBindable, EventHandler> removeHandler,
EventTrigger trigger)
: IEventSubscriptionStrategy
where TBindable : BindableObject
{
if (_eventinfo != null && _handlerdelegate != null)
_eventinfo.RemoveEventHandler(bindable, _handlerdelegate);
public void AttachHandlerTo(BindableObject target)
{
if (target is TBindable typedTarget)
addHandler(typedTarget, InvokeActions);
}

public void DetachHandlerFrom(BindableObject target)
{
if (target is TBindable typedTarget)
removeHandler(typedTarget, InvokeActions);
}

private void InvokeActions(object? sender, EventArgs e) => trigger.InvokeActions(sender);
}

void OnEventTriggered(object sender, EventArgs e)
private sealed class StaticStrategy<TBindable, TEventArgs>(
Action<TBindable, EventHandler<TEventArgs>> addHandler,
Action<TBindable, EventHandler<TEventArgs>> removeHandler,
EventTrigger trigger)
: IEventSubscriptionStrategy
where TBindable : BindableObject
where TEventArgs : EventArgs
{
var bindable = (BindableObject)sender;
foreach (TriggerAction action in Actions)
action.DoInvoke(bindable);
public void AttachHandlerTo(BindableObject target)
{
if (target is TBindable typedTarget)
addHandler(typedTarget, InvokeActions);
}

public void DetachHandlerFrom(BindableObject target)
{
if (target is TBindable typedTarget)
removeHandler(typedTarget, InvokeActions);
}

private void InvokeActions(object? sender, TEventArgs e) => trigger.InvokeActions(sender);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui
~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource>.OnInterceptTouchEvent(Android.Views.MotionEvent e) -> bool
~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource>.OnTouchEvent(Android.Views.MotionEvent e) -> bool
~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable>(string! eventName, System.Action<TBindable!, System.EventHandler!>! addHandler, System.Action<TBindable!, System.EventHandler!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable, TEventArgs>(string! eventName, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! addHandler, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui
~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string
~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void
~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable>(string! eventName, System.Action<TBindable!, System.EventHandler!>! addHandler, System.Action<TBindable!, System.EventHandler!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable, TEventArgs>(string! eventName, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! addHandler, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui
~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string
~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void
~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable>(string! eventName, System.Action<TBindable!, System.EventHandler!>! addHandler, System.Action<TBindable!, System.EventHandler!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable, TEventArgs>(string! eventName, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! addHandler, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui
~Microsoft.Maui.Controls.ResourceDictionary.AddFactory(string key, System.Func<object> factory, bool shared = true) -> void
~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string
~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable>(string! eventName, System.Action<TBindable!, System.EventHandler!>! addHandler, System.Action<TBindable!, System.EventHandler!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable, TEventArgs>(string! eventName, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! addHandler, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui
~Microsoft.Maui.Controls.ResourceDictionary.AddFactory(string key, System.Func<object> factory, bool shared = true) -> void
~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string
~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable>(string! eventName, System.Action<TBindable!, System.EventHandler!>! addHandler, System.Action<TBindable!, System.EventHandler!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable, TEventArgs>(string! eventName, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! addHandler, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
2 changes: 2 additions & 0 deletions src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui
~Microsoft.Maui.Controls.ResourceDictionary.AddFactory(string key, System.Func<object> factory, bool shared = true) -> void
~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string
~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable>(string! eventName, System.Action<TBindable!, System.EventHandler!>! addHandler, System.Action<TBindable!, System.EventHandler!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable, TEventArgs>(string! eventName, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! addHandler, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui
~Microsoft.Maui.Controls.ResourceDictionary.AddFactory(System.Type targetType, System.Func<Microsoft.Maui.Controls.Style> factory, bool shared = true) -> void
~Microsoft.Maui.Controls.ResourceDictionary.AddFactory(string key, System.Func<object> factory, bool shared = true) -> void
~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable>(string! eventName, System.Action<TBindable!, System.EventHandler!>! addHandler, System.Action<TBindable!, System.EventHandler!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
static Microsoft.Maui.Controls.EventTrigger.Create<TBindable, TEventArgs>(string! eventName, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! addHandler, System.Action<TBindable!, System.EventHandler<TEventArgs!>!>! removeHandler) -> Microsoft.Maui.Controls.EventTrigger!
2 changes: 2 additions & 0 deletions src/Controls/src/SourceGen/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ MAUIX2011 | XamlParsing | Warning | AmbiguousMemberWithStaticType
MAUIX2012 | XamlParsing | Error | CSharpExpressionsRequirePreviewFeatures
MAUIX2013 | XamlParsing | Error | AsyncLambdaNotSupported
MAUIX2015 | XamlInflation | Warning | DuplicatePropertyAssignment
MAUIX2016 | XamlInflation | Error | EventTriggerEventNotFound
MAUIX2017 | XamlInflation | Warning | EventTriggerTargetTypeNotResolved
16 changes: 16 additions & 0 deletions src/Controls/src/SourceGen/Descriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,22 @@ public static class Descriptors
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static DiagnosticDescriptor EventTriggerEventNotFound = new DiagnosticDescriptor(
id: "MAUIX2016",
title: new LocalizableResourceString(nameof(MauiGResources.EventTriggerEventNotFoundTitle), MauiGResources.ResourceManager, typeof(MauiGResources)),
messageFormat: new LocalizableResourceString(nameof(MauiGResources.EventTriggerEventNotFoundMessage), MauiGResources.ResourceManager, typeof(MauiGResources)),
category: "XamlInflation",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor EventTriggerTargetTypeNotResolved = new DiagnosticDescriptor(
id: "MAUIX2017",
title: new LocalizableResourceString(nameof(MauiGResources.EventTriggerTargetTypeNotResolvedTitle), MauiGResources.ResourceManager, typeof(MauiGResources)),
messageFormat: new LocalizableResourceString(nameof(MauiGResources.EventTriggerTargetTypeNotResolvedMessage), MauiGResources.ResourceManager, typeof(MauiGResources)),
category: "XamlInflation",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

// public static BuildExceptionCode TypeResolution = new BuildExceptionCode("XC", 0000, nameof(TypeResolution), "");
// public static BuildExceptionCode PropertyResolution = new BuildExceptionCode("XC", 0001, nameof(PropertyResolution), "");
// public static BuildExceptionCode MissingEventHandler = new BuildExceptionCode("XC", 0002, nameof(MissingEventHandler), "");
Expand Down
Loading
Loading