using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Reactive;
namespace Avalonia.Xaml.Interactions.Core;
///
/// An action that calls a method on a specified object when invoked.
///
[RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
public class CallMethodAction : Avalonia.Xaml.Interactivity.Action
{
private Type? _targetObjectType;
private readonly List _methodDescriptors = [];
private MethodDescriptor? _cachedMethodDescriptor;
///
/// Identifies the avalonia property.
///
public static readonly StyledProperty MethodNameProperty =
AvaloniaProperty.Register(nameof(MethodName));
///
/// Identifies the avalonia property.
///
public static readonly StyledProperty TargetObjectProperty =
AvaloniaProperty.Register(nameof(TargetObject));
///
/// Gets or sets the name of the method to invoke. This is a avalonia property.
///
public string? MethodName
{
get => GetValue(MethodNameProperty);
set => SetValue(MethodNameProperty, value);
}
///
/// Gets or sets the object that exposes the method of interest. This is a avalonia property.
///
[ResolveByName]
public object? TargetObject
{
get => GetValue(TargetObjectProperty);
set => SetValue(TargetObjectProperty, value);
}
static CallMethodAction()
{
MethodNameProperty.Changed.Subscribe(
new AnonymousObserver>(MethodNameChanged));
TargetObjectProperty.Changed.Subscribe(
new AnonymousObserver>(TargetObjectChanged));
}
private static void MethodNameChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is not CallMethodAction callMethodAction)
{
return;
}
callMethodAction.UpdateMethodDescriptors();
}
[RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
private static void TargetObjectChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is not CallMethodAction callMethodAction)
{
return;
}
var newValue = e.NewValue.GetValueOrDefault();
if (newValue is not null)
{
var newType = newValue.GetType();
callMethodAction.UpdateTargetType(newType);
}
}
///
/// Executes the action.
///
/// The that is passed to the action by the behavior. Generally this is or a target object.
/// The value of this parameter is determined by the caller.
/// True if the method is called; else false.
public override object Execute(object? sender, object? parameter)
{
if (!IsEnabled)
{
return false;
}
var target = GetValue(TargetObjectProperty) is not null ? TargetObject : sender;
if (target is null || string.IsNullOrEmpty(MethodName))
{
return false;
}
UpdateTargetType(target.GetType());
var methodDescriptor = FindBestMethod(parameter);
if (methodDescriptor is null)
{
if (TargetObject is not null)
{
throw new ArgumentException(string.Format(
CultureInfo.CurrentCulture,
"Cannot find method named {0} on object of type {1} that matches the expected signature.",
MethodName,
_targetObjectType));
}
return false;
}
var parameters = methodDescriptor.Parameters;
switch (parameters.Length)
{
case 0:
methodDescriptor.MethodInfo.Invoke(target, null);
return true;
case 2:
methodDescriptor.MethodInfo.Invoke(target, [target, parameter!]);
return true;
default:
return false;
}
}
[RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
private MethodDescriptor? FindBestMethod(object? parameter)
{
if (parameter is null)
{
return _cachedMethodDescriptor;
}
var parameterTypeInfo = parameter.GetType().GetTypeInfo();
MethodDescriptor? mostDerivedMethod = null;
// Loop over the methods looking for the one whose type is closest to the type of the given parameter.
foreach (var currentMethod in _methodDescriptors)
{
var currentTypeInfo = currentMethod.SecondParameterTypeInfo;
if (currentTypeInfo is not null && currentTypeInfo.IsAssignableFrom(parameterTypeInfo))
{
if (mostDerivedMethod is null || !currentTypeInfo.IsAssignableFrom(mostDerivedMethod.SecondParameterTypeInfo))
{
mostDerivedMethod = currentMethod;
}
}
}
return mostDerivedMethod ?? _cachedMethodDescriptor;
}
private void UpdateTargetType(Type newTargetType)
{
if (newTargetType == _targetObjectType)
{
return;
}
_targetObjectType = newTargetType;
UpdateMethodDescriptors();
}
[RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
private void UpdateMethodDescriptors()
{
_methodDescriptors.Clear();
_cachedMethodDescriptor = null;
if (string.IsNullOrEmpty(MethodName) || _targetObjectType is null)
{
return;
}
// Find all public methods that match the given name and have either no parameters,
// or two parameters where the first is of type Object.
foreach (var method in _targetObjectType.GetRuntimeMethods())
{
if (string.Equals(method.Name, MethodName, StringComparison.Ordinal)
&& (method.ReturnType == typeof(void) || method.ReturnType == typeof(Task))
&& method.IsPublic)
{
var parameters = method.GetParameters();
if (parameters.Length == 0)
{
// There can be only one parameterless method of the given name.
_cachedMethodDescriptor = new MethodDescriptor(method, parameters);
}
else if (parameters.Length == 2 && parameters[0].ParameterType == typeof(object))
{
_methodDescriptors.Add(new MethodDescriptor(method, parameters));
}
}
}
// We didn't find a parameterless method, so we want to find a method that accepts null
// as a second parameter, but if we have more than one of these it is ambiguous which
// we should call, so we do nothing.
if (_cachedMethodDescriptor is null)
{
foreach (var method in _methodDescriptors)
{
var typeInfo = method.SecondParameterTypeInfo;
if (typeInfo is not null && (!typeInfo.IsValueType || typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)))
{
if (_cachedMethodDescriptor is not null)
{
_cachedMethodDescriptor = null;
return;
}
_cachedMethodDescriptor = method;
}
}
}
}
[RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
[DebuggerDisplay($"{{{nameof(MethodInfo)}}}")]
private class MethodDescriptor(MethodInfo methodInfo, ParameterInfo[] methodParameters)
{
public MethodInfo MethodInfo { get; private set; } = methodInfo;
public ParameterInfo[] Parameters { get; private set; } = methodParameters;
public int ParameterCount => Parameters.Length;
public TypeInfo? SecondParameterTypeInfo
{
get => ParameterCount < 2 ? null : Parameters[1].ParameterType.GetTypeInfo();
}
}
}