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;
}