using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using Avalonia.Xaml.Interactivity; using Avalonia.Controls; using Avalonia.Reactive; namespace Avalonia.Xaml.Interactions.Core; /// /// A behavior that listens for a specified event on its source and executes its actions when that event is fired. /// [RequiresUnreferencedCode("This functionality is not compatible with trimming.")] public class EventTriggerBehavior : StyledElementTrigger { private const string EventNameDefaultValue = "AttachedToVisualTree"; /// /// Identifies the avalonia property. /// public static readonly StyledProperty EventNameProperty = AvaloniaProperty.Register(nameof(EventName), EventNameDefaultValue); /// /// Identifies the avalonia property. /// public static readonly StyledProperty SourceObjectProperty = AvaloniaProperty.Register(nameof(SourceObject)); private object? _resolvedSource; private Delegate? _eventHandler; private bool _isLoadedEventRegistered; /// /// Gets or sets the name of the event to listen for. This is a avalonia property. /// public string? EventName { get => GetValue(EventNameProperty); set => SetValue(EventNameProperty, value); } /// /// Gets or sets the source object from which this behavior listens for events. /// If is not set, the source will default to . This is a avalonia property. /// [ResolveByName] public object? SourceObject { get => GetValue(SourceObjectProperty); set => SetValue(SourceObjectProperty, value); } static EventTriggerBehavior() { EventNameProperty.Changed.Subscribe( new AnonymousObserver>(EventNameChanged)); SourceObjectProperty.Changed.Subscribe( new AnonymousObserver>(SourceObjectChanged)); } [RequiresUnreferencedCode("This functionality is not compatible with trimming.")] private static void EventNameChanged(AvaloniaPropertyChangedEventArgs e) { if (e.Sender is not EventTriggerBehavior behavior) { return; } if (behavior.AssociatedObject is null || behavior._resolvedSource is null) { return; } var oldEventName = e.OldValue.GetValueOrDefault(); var newEventName = e.NewValue.GetValueOrDefault(); if (oldEventName is not null) { behavior.UnregisterEvent(oldEventName); } if (newEventName is not null) { behavior.RegisterEvent(newEventName); } } private static void SourceObjectChanged(AvaloniaPropertyChangedEventArgs e) { if (e.Sender is EventTriggerBehavior behavior) { behavior.SetResolvedSource(behavior.ComputeResolvedSource()); } } /// /// Called after the behavior is attached to the . /// protected override void OnAttached() { base.OnAttached(); SetResolvedSource(ComputeResolvedSource()); } /// /// Called when the behavior is being detached from its . /// protected override void OnDetaching() { base.OnDetaching(); SetResolvedSource(null); } private void SetResolvedSource(object? newSource) { if (AssociatedObject is null || _resolvedSource == newSource) { return; } if (_resolvedSource is not null) { UnregisterEvent(EventName); } _resolvedSource = newSource; if (_resolvedSource is not null) { RegisterEvent(EventName); } } private object? ComputeResolvedSource() { // If the SourceObject property is set at all, we want to use it. It is possible that it is data // bound and bindings haven't been evaluated yet. Plus, this makes the API more predictable. if (GetValue(SourceObjectProperty) is not null) { return SourceObject; } return AssociatedObject; } [RequiresUnreferencedCode("This functionality is not compatible with trimming.")] private void RegisterEvent(string? eventName) { if (string.IsNullOrEmpty(eventName)) { return; } if (eventName != EventNameDefaultValue) { if (_resolvedSource is null) { return; } if (EventName is null) { return; } var sourceObjectType = _resolvedSource.GetType(); var eventInfo = sourceObjectType.GetRuntimeEvent(EventName); if (eventInfo is null) { throw new ArgumentException(string.Format( CultureInfo.CurrentCulture, "Cannot find an event named {0} on type {1}.", EventName, sourceObjectType.Name)); } var methodInfo = typeof(EventTriggerBehavior).GetTypeInfo().GetDeclaredMethod("AttachedToVisualTree"); if (methodInfo is not null) { var eventHandlerType = eventInfo.EventHandlerType; if (eventHandlerType is not null) { _eventHandler = methodInfo.CreateDelegate(eventHandlerType, this); if (_eventHandler is not null) { eventInfo.AddEventHandler(_resolvedSource, _eventHandler); } } } } else if (!_isLoadedEventRegistered) { if (_resolvedSource is Control element && !IsElementLoaded(element)) { _isLoadedEventRegistered = true; element.AttachedToVisualTree += AttachedToVisualTree; } } } [RequiresUnreferencedCode("This functionality is not compatible with trimming.")] private void UnregisterEvent(string? eventName) { if (string.IsNullOrEmpty(eventName)) { return; } if (eventName != EventNameDefaultValue) { if (_eventHandler is null) { return; } if (_resolvedSource is not null) { var eventInfo = _resolvedSource.GetType().GetRuntimeEvent(eventName); eventInfo?.RemoveEventHandler(_resolvedSource, _eventHandler); } _eventHandler = null; } else if (_isLoadedEventRegistered) { _isLoadedEventRegistered = false; if (_resolvedSource is Control element) { element.AttachedToVisualTree -= AttachedToVisualTree; } } } /// /// Raised when the control is attached to a rooted visual tree. /// /// The sender object. /// The event args. protected virtual void AttachedToVisualTree(object? sender, object eventArgs) { if (!IsEnabled) { return; } Interaction.ExecuteActions(_resolvedSource, Actions, eventArgs); } private static bool IsElementLoaded(Control element) => element.Parent is not null; }