CallMethodAction.cs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Globalization;
  6. using System.Reflection;
  7. using System.Threading.Tasks;
  8. using Avalonia.Controls;
  9. using Avalonia.Reactive;
  10. namespace Avalonia.Xaml.Interactions.Core;
  11. /// <summary>
  12. /// An action that calls a method on a specified object when invoked.
  13. /// </summary>
  14. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  15. public class CallMethodAction : Avalonia.Xaml.Interactivity.Action
  16. {
  17. private Type? _targetObjectType;
  18. private readonly List<MethodDescriptor> _methodDescriptors = [];
  19. private MethodDescriptor? _cachedMethodDescriptor;
  20. /// <summary>
  21. /// Identifies the <seealso cref="MethodName"/> avalonia property.
  22. /// </summary>
  23. public static readonly StyledProperty<string?> MethodNameProperty =
  24. AvaloniaProperty.Register<CallMethodAction, string?>(nameof(MethodName));
  25. /// <summary>
  26. /// Identifies the <seealso cref="TargetObject"/> avalonia property.
  27. /// </summary>
  28. public static readonly StyledProperty<object?> TargetObjectProperty =
  29. AvaloniaProperty.Register<CallMethodAction, object?>(nameof(TargetObject));
  30. /// <summary>
  31. /// Gets or sets the name of the method to invoke. This is a avalonia property.
  32. /// </summary>
  33. public string? MethodName
  34. {
  35. get => GetValue(MethodNameProperty);
  36. set => SetValue(MethodNameProperty, value);
  37. }
  38. /// <summary>
  39. /// Gets or sets the object that exposes the method of interest. This is a avalonia property.
  40. /// </summary>
  41. [ResolveByName]
  42. public object? TargetObject
  43. {
  44. get => GetValue(TargetObjectProperty);
  45. set => SetValue(TargetObjectProperty, value);
  46. }
  47. static CallMethodAction()
  48. {
  49. MethodNameProperty.Changed.Subscribe(
  50. new AnonymousObserver<AvaloniaPropertyChangedEventArgs<string?>>(MethodNameChanged));
  51. TargetObjectProperty.Changed.Subscribe(
  52. new AnonymousObserver<AvaloniaPropertyChangedEventArgs<object?>>(TargetObjectChanged));
  53. }
  54. private static void MethodNameChanged(AvaloniaPropertyChangedEventArgs<string?> e)
  55. {
  56. if (e.Sender is not CallMethodAction callMethodAction)
  57. {
  58. return;
  59. }
  60. callMethodAction.UpdateMethodDescriptors();
  61. }
  62. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  63. private static void TargetObjectChanged(AvaloniaPropertyChangedEventArgs<object?> e)
  64. {
  65. if (e.Sender is not CallMethodAction callMethodAction)
  66. {
  67. return;
  68. }
  69. var newValue = e.NewValue.GetValueOrDefault();
  70. if (newValue is not null)
  71. {
  72. var newType = newValue.GetType();
  73. callMethodAction.UpdateTargetType(newType);
  74. }
  75. }
  76. /// <summary>
  77. /// Executes the action.
  78. /// </summary>
  79. /// <param name="sender">The <see cref="object"/> that is passed to the action by the behavior. Generally this is <seealso cref="Avalonia.Xaml.Interactivity.IBehavior.AssociatedObject"/> or a target object.</param>
  80. /// <param name="parameter">The value of this parameter is determined by the caller.</param>
  81. /// <returns>True if the method is called; else false.</returns>
  82. public override object Execute(object? sender, object? parameter)
  83. {
  84. if (!IsEnabled)
  85. {
  86. return false;
  87. }
  88. var target = GetValue(TargetObjectProperty) is not null ? TargetObject : sender;
  89. if (target is null || string.IsNullOrEmpty(MethodName))
  90. {
  91. return false;
  92. }
  93. UpdateTargetType(target.GetType());
  94. var methodDescriptor = FindBestMethod(parameter);
  95. if (methodDescriptor is null)
  96. {
  97. if (TargetObject is not null)
  98. {
  99. throw new ArgumentException(string.Format(
  100. CultureInfo.CurrentCulture,
  101. "Cannot find method named {0} on object of type {1} that matches the expected signature.",
  102. MethodName,
  103. _targetObjectType));
  104. }
  105. return false;
  106. }
  107. var parameters = methodDescriptor.Parameters;
  108. switch (parameters.Length)
  109. {
  110. case 0:
  111. methodDescriptor.MethodInfo.Invoke(target, null);
  112. return true;
  113. case 2:
  114. methodDescriptor.MethodInfo.Invoke(target, [target, parameter!]);
  115. return true;
  116. default:
  117. return false;
  118. }
  119. }
  120. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  121. private MethodDescriptor? FindBestMethod(object? parameter)
  122. {
  123. if (parameter is null)
  124. {
  125. return _cachedMethodDescriptor;
  126. }
  127. var parameterTypeInfo = parameter.GetType().GetTypeInfo();
  128. MethodDescriptor? mostDerivedMethod = null;
  129. // Loop over the methods looking for the one whose type is closest to the type of the given parameter.
  130. foreach (var currentMethod in _methodDescriptors)
  131. {
  132. var currentTypeInfo = currentMethod.SecondParameterTypeInfo;
  133. if (currentTypeInfo is not null && currentTypeInfo.IsAssignableFrom(parameterTypeInfo))
  134. {
  135. if (mostDerivedMethod is null || !currentTypeInfo.IsAssignableFrom(mostDerivedMethod.SecondParameterTypeInfo))
  136. {
  137. mostDerivedMethod = currentMethod;
  138. }
  139. }
  140. }
  141. return mostDerivedMethod ?? _cachedMethodDescriptor;
  142. }
  143. private void UpdateTargetType(Type newTargetType)
  144. {
  145. if (newTargetType == _targetObjectType)
  146. {
  147. return;
  148. }
  149. _targetObjectType = newTargetType;
  150. UpdateMethodDescriptors();
  151. }
  152. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  153. private void UpdateMethodDescriptors()
  154. {
  155. _methodDescriptors.Clear();
  156. _cachedMethodDescriptor = null;
  157. if (string.IsNullOrEmpty(MethodName) || _targetObjectType is null)
  158. {
  159. return;
  160. }
  161. // Find all public methods that match the given name and have either no parameters,
  162. // or two parameters where the first is of type Object.
  163. foreach (var method in _targetObjectType.GetRuntimeMethods())
  164. {
  165. if (string.Equals(method.Name, MethodName, StringComparison.Ordinal)
  166. && (method.ReturnType == typeof(void) || method.ReturnType == typeof(Task))
  167. && method.IsPublic)
  168. {
  169. var parameters = method.GetParameters();
  170. if (parameters.Length == 0)
  171. {
  172. // There can be only one parameterless method of the given name.
  173. _cachedMethodDescriptor = new MethodDescriptor(method, parameters);
  174. }
  175. else if (parameters.Length == 2 && parameters[0].ParameterType == typeof(object))
  176. {
  177. _methodDescriptors.Add(new MethodDescriptor(method, parameters));
  178. }
  179. }
  180. }
  181. // We didn't find a parameterless method, so we want to find a method that accepts null
  182. // as a second parameter, but if we have more than one of these it is ambiguous which
  183. // we should call, so we do nothing.
  184. if (_cachedMethodDescriptor is null)
  185. {
  186. foreach (var method in _methodDescriptors)
  187. {
  188. var typeInfo = method.SecondParameterTypeInfo;
  189. if (typeInfo is not null && (!typeInfo.IsValueType || typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)))
  190. {
  191. if (_cachedMethodDescriptor is not null)
  192. {
  193. _cachedMethodDescriptor = null;
  194. return;
  195. }
  196. _cachedMethodDescriptor = method;
  197. }
  198. }
  199. }
  200. }
  201. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  202. [DebuggerDisplay($"{{{nameof(MethodInfo)}}}")]
  203. private class MethodDescriptor(MethodInfo methodInfo, ParameterInfo[] methodParameters)
  204. {
  205. public MethodInfo MethodInfo { get; private set; } = methodInfo;
  206. public ParameterInfo[] Parameters { get; private set; } = methodParameters;
  207. public int ParameterCount => Parameters.Length;
  208. public TypeInfo? SecondParameterTypeInfo
  209. {
  210. get => ParameterCount < 2 ? null : Parameters[1].ParameterType.GetTypeInfo();
  211. }
  212. }
  213. }