EventTriggerBehavior.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. using System;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Globalization;
  4. using System.Reflection;
  5. using Avalonia.Xaml.Interactivity;
  6. using Avalonia.Controls;
  7. using Avalonia.Reactive;
  8. namespace Avalonia.Xaml.Interactions.Core;
  9. /// <summary>
  10. /// A behavior that listens for a specified event on its source and executes its actions when that event is fired.
  11. /// </summary>
  12. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  13. public class EventTriggerBehavior : StyledElementTrigger
  14. {
  15. private const string EventNameDefaultValue = "AttachedToVisualTree";
  16. /// <summary>
  17. /// Identifies the <seealso cref="EventName"/> avalonia property.
  18. /// </summary>
  19. public static readonly StyledProperty<string?> EventNameProperty =
  20. AvaloniaProperty.Register<EventTriggerBehavior, string?>(nameof(EventName), EventNameDefaultValue);
  21. /// <summary>
  22. /// Identifies the <seealso cref="SourceObject"/> avalonia property.
  23. /// </summary>
  24. public static readonly StyledProperty<object?> SourceObjectProperty =
  25. AvaloniaProperty.Register<EventTriggerBehavior, object?>(nameof(SourceObject));
  26. private object? _resolvedSource;
  27. private Delegate? _eventHandler;
  28. private bool _isLoadedEventRegistered;
  29. /// <summary>
  30. /// Gets or sets the name of the event to listen for. This is a avalonia property.
  31. /// </summary>
  32. public string? EventName
  33. {
  34. get => GetValue(EventNameProperty);
  35. set => SetValue(EventNameProperty, value);
  36. }
  37. /// <summary>
  38. /// Gets or sets the source object from which this behavior listens for events.
  39. /// If <seealso cref="SourceObject"/> is not set, the source will default to <seealso cref="IBehavior.AssociatedObject"/>. This is a avalonia property.
  40. /// </summary>
  41. [ResolveByName]
  42. public object? SourceObject
  43. {
  44. get => GetValue(SourceObjectProperty);
  45. set => SetValue(SourceObjectProperty, value);
  46. }
  47. static EventTriggerBehavior()
  48. {
  49. EventNameProperty.Changed.Subscribe(
  50. new AnonymousObserver<AvaloniaPropertyChangedEventArgs<string?>>(EventNameChanged));
  51. SourceObjectProperty.Changed.Subscribe(
  52. new AnonymousObserver<AvaloniaPropertyChangedEventArgs<object?>>(SourceObjectChanged));
  53. }
  54. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  55. private static void EventNameChanged(AvaloniaPropertyChangedEventArgs<string?> e)
  56. {
  57. if (e.Sender is not EventTriggerBehavior behavior)
  58. {
  59. return;
  60. }
  61. if (behavior.AssociatedObject is null || behavior._resolvedSource is null)
  62. {
  63. return;
  64. }
  65. var oldEventName = e.OldValue.GetValueOrDefault();
  66. var newEventName = e.NewValue.GetValueOrDefault();
  67. if (oldEventName is not null)
  68. {
  69. behavior.UnregisterEvent(oldEventName);
  70. }
  71. if (newEventName is not null)
  72. {
  73. behavior.RegisterEvent(newEventName);
  74. }
  75. }
  76. private static void SourceObjectChanged(AvaloniaPropertyChangedEventArgs<object?> e)
  77. {
  78. if (e.Sender is EventTriggerBehavior behavior)
  79. {
  80. behavior.SetResolvedSource(behavior.ComputeResolvedSource());
  81. }
  82. }
  83. /// <summary>
  84. /// Called after the behavior is attached to the <see cref="IBehavior.AssociatedObject"/>.
  85. /// </summary>
  86. protected override void OnAttached()
  87. {
  88. base.OnAttached();
  89. SetResolvedSource(ComputeResolvedSource());
  90. }
  91. /// <summary>
  92. /// Called when the behavior is being detached from its <see cref="IBehavior.AssociatedObject"/>.
  93. /// </summary>
  94. protected override void OnDetaching()
  95. {
  96. base.OnDetaching();
  97. SetResolvedSource(null);
  98. }
  99. private void SetResolvedSource(object? newSource)
  100. {
  101. if (AssociatedObject is null || _resolvedSource == newSource)
  102. {
  103. return;
  104. }
  105. if (_resolvedSource is not null)
  106. {
  107. UnregisterEvent(EventName);
  108. }
  109. _resolvedSource = newSource;
  110. if (_resolvedSource is not null)
  111. {
  112. RegisterEvent(EventName);
  113. }
  114. }
  115. private object? ComputeResolvedSource()
  116. {
  117. // If the SourceObject property is set at all, we want to use it. It is possible that it is data
  118. // bound and bindings haven't been evaluated yet. Plus, this makes the API more predictable.
  119. if (GetValue(SourceObjectProperty) is not null)
  120. {
  121. return SourceObject;
  122. }
  123. return AssociatedObject;
  124. }
  125. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  126. private void RegisterEvent(string? eventName)
  127. {
  128. if (string.IsNullOrEmpty(eventName))
  129. {
  130. return;
  131. }
  132. if (eventName != EventNameDefaultValue)
  133. {
  134. if (_resolvedSource is null)
  135. {
  136. return;
  137. }
  138. if (EventName is null)
  139. {
  140. return;
  141. }
  142. var sourceObjectType = _resolvedSource.GetType();
  143. var eventInfo = sourceObjectType.GetRuntimeEvent(EventName);
  144. if (eventInfo is null)
  145. {
  146. throw new ArgumentException(string.Format(
  147. CultureInfo.CurrentCulture,
  148. "Cannot find an event named {0} on type {1}.",
  149. EventName,
  150. sourceObjectType.Name));
  151. }
  152. var methodInfo = typeof(EventTriggerBehavior).GetTypeInfo().GetDeclaredMethod("AttachedToVisualTree");
  153. if (methodInfo is not null)
  154. {
  155. var eventHandlerType = eventInfo.EventHandlerType;
  156. if (eventHandlerType is not null)
  157. {
  158. _eventHandler = methodInfo.CreateDelegate(eventHandlerType, this);
  159. if (_eventHandler is not null)
  160. {
  161. eventInfo.AddEventHandler(_resolvedSource, _eventHandler);
  162. }
  163. }
  164. }
  165. }
  166. else if (!_isLoadedEventRegistered)
  167. {
  168. if (_resolvedSource is Control element && !IsElementLoaded(element))
  169. {
  170. _isLoadedEventRegistered = true;
  171. element.AttachedToVisualTree += AttachedToVisualTree;
  172. }
  173. }
  174. }
  175. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  176. private void UnregisterEvent(string? eventName)
  177. {
  178. if (string.IsNullOrEmpty(eventName))
  179. {
  180. return;
  181. }
  182. if (eventName != EventNameDefaultValue)
  183. {
  184. if (_eventHandler is null)
  185. {
  186. return;
  187. }
  188. if (_resolvedSource is not null)
  189. {
  190. var eventInfo = _resolvedSource.GetType().GetRuntimeEvent(eventName);
  191. eventInfo?.RemoveEventHandler(_resolvedSource, _eventHandler);
  192. }
  193. _eventHandler = null;
  194. }
  195. else if (_isLoadedEventRegistered)
  196. {
  197. _isLoadedEventRegistered = false;
  198. if (_resolvedSource is Control element)
  199. {
  200. element.AttachedToVisualTree -= AttachedToVisualTree;
  201. }
  202. }
  203. }
  204. /// <summary>
  205. /// Raised when the control is attached to a rooted visual tree.
  206. /// </summary>
  207. /// <param name="sender">The sender object.</param>
  208. /// <param name="eventArgs">The event args.</param>
  209. protected virtual void AttachedToVisualTree(object? sender, object eventArgs)
  210. {
  211. if (!IsEnabled)
  212. {
  213. return;
  214. }
  215. Interaction.ExecuteActions(_resolvedSource, Actions, eventArgs);
  216. }
  217. private static bool IsElementLoaded(Control element) => element.Parent is not null;
  218. }