File: TypeConversionHelper.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable enable
 
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Hosting;
 
namespace Microsoft.Maui.Controls
{
	internal static class TypeConversionHelper
	{
		internal static bool TryConvert(object value, Type targetType, out object? convertedValue)
		{
			Type valueType = value.GetType();
 
			if (value is IWrappedValue { Value: var wrappedValue, ValueType: var wrappedType })
			{
				if (wrappedValue is null)
				{
					convertedValue = null;
					return !targetType.IsValueType || targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>);
				}
 
				// It might be enough just to unwrap the value
				if (targetType.IsAssignableFrom(wrappedType))
				{
					convertedValue = wrappedValue;
					return true;
				}
 
				value = wrappedValue;
				valueType = wrappedType;
			}
 
			if (RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported)
			{
#if NET8_0
#pragma warning disable IL2026 // FeatureGuardAttribute is not supported on .NET 8
#endif
				if (TryConvertUsingImplicitCastOperator(value, targetType, out convertedValue))
#if NET8_0
#pragma warning restore IL2026 // FeatureGuardAttribute is not supported on .NET 8
#endif
				{
					return true;
				}
			}
			else
			{
				if (TryGetTypeConverter(valueType, out var converter) && converter is not null && converter.CanConvertTo(targetType))
				{
					convertedValue = converter.ConvertTo(value, targetType) ?? throw new InvalidOperationException($"The {converter.GetType()} returned null when converting {valueType} to {targetType}");
					return true;
				}
 
				if (TryGetTypeConverter(targetType, out converter) && converter is not null && converter.CanConvertFrom(valueType))
				{
					convertedValue = converter.ConvertFrom(value) ?? throw new InvalidOperationException($"The {converter.GetType()} returned null when converting from {valueType}");
					return true;
				}
 
				WarnIfImplicitOperatorIsAvailable(value, targetType);
			}
 
			convertedValue = null;
			return false;
		}
 
		private static bool TryGetTypeConverter(Type type, [NotNullWhen(true)] out TypeConverter? converter)
			=> type.TryGetTypeConverter(out converter);
 
		private static bool ShouldCheckForImplicitConversionOperator(Type type) =>
			type != typeof(string) && !BindableProperty.SimpleConvertTypes.ContainsKey(type);
 
		[RequiresUnreferencedCode("The method uses reflection to find implicit conversion operators. " +
			"It is not possible to guarantee that trimming does not remove some of the implicit operators. " +
			"Consider adding a custom TypeConverter that can perform the conversion instead.")]
		private static bool TryConvertUsingImplicitCastOperator(object value, Type targetType, [NotNullWhen(true)] out object? result)
		{
			Type valueType = value.GetType();
			MethodInfo? opImplicit = GetImplicitConversionOperator(valueType, fromType: valueType, toType: targetType)
										?? GetImplicitConversionOperator(targetType, fromType: valueType, toType: targetType);
 
			object? convertedValue = opImplicit?.Invoke(null, new[] { value });
			if (convertedValue is not null)
			{
				result = convertedValue;
				return true;
			}
 
			result = null;
			return false;
 
			[RequiresUnreferencedCode("The method looks for op_Implicit methods using reflection. " +
				"We cannot guarantee that the method is preserved when the code is trimmed.")]
			static MethodInfo? GetImplicitConversionOperator(Type onType, Type fromType, Type toType)
			{
				if (!ShouldCheckForImplicitConversionOperator(onType))
					return null;
 
				const BindingFlags bindingAttr = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;
 
				try
				{
					var method = onType.GetMethod("op_Implicit", bindingAttr, null, new[] { fromType }, null);
					if (method is not null
						&& IsImplicitOperator(method)
						&& HasSuitableReturnType(method, toType))
					{
						return method;
					}
				}
				catch (AmbiguousMatchException)
				{
					// Ignore the exception and fall back to custom filtering of all methods.
					// This happens when there are multiple implicit operators with the same parameter type,
					// but with different return types.
				}
 
				foreach (var method in onType.GetMethods(bindingAttr))
				{
					if (IsImplicitOperator(method)
						&& HasSuitableParameterType(method, fromType)
						&& HasSuitableReturnType(method, toType))
					{
						return method;
					}
				}
 
				return null;
			}
 
			static bool IsImplicitOperator(MethodInfo method)
				=> method.IsSpecialName
					&& method.IsPublic
					&& method.IsStatic
					&& method.Name == "op_Implicit";
 
			// Note: our custom type compatiblity checks are much less permissie than what .NET does internally.
			// The `IsAssignableFrom` method will for example return `false` when asking if `int` can be assigned
			// to `long`. This will filter out some valid matches and can lead to unexpected behavior.
			// The converison method has behaved this way for many years so we should probably keep it this way,
			// especially since we're trying to replace these implicit operators with type converters.
 
			static bool HasSuitableParameterType(MethodInfo method, Type fromType)
				=> method.GetParameters() is ParameterInfo[] parameters
					&& parameters.Length == 1
					&& parameters[0].ParameterType.IsAssignableFrom(fromType);
 
			static bool HasSuitableReturnType(MethodInfo method, Type toType)
				=> toType.IsAssignableFrom(method.ReturnType);
		}
 
		private static void WarnIfImplicitOperatorIsAvailable(object value, Type targetType)
		{
			[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
				Justification = "The method tries finding implicit cast operators reflection to help developers " +
					"catch the cases where they are missing type converters during development mostly in debug builds. " +
					"The method is expected not to find anything when the app code is trimmed.")]
			bool HasImplicitOperatorConversion()
			{
				return TryConvertUsingImplicitCastOperator(value, targetType, out _);
			}
 
			if (HasImplicitOperatorConversion())
			{
				// If we reach this point, it means that the implicit cast operator exists, but we are not allowed to use it. This can happen for example in debug builds
				// when the app is not trimmed. This is the best effort to help developers catch the cases where they are missing type converters during development.
				// Unforutnately, we cannot just add a build warning at this moment.
				Application.Current?.FindMauiContext()?.CreateLogger(nameof(TypeConversionHelper))?.LogWarning(
					$"It is not possible to convert value of type {value.GetType()} to {targetType} via an implicit cast " +
					"because this feature is disabled. You should add a type converter that will implement this conversion and attach it to either of " +
					"these types using the [TypeConverter] attribute. Alternatively, you " +
					"can enable this feature by setting the MauiImplicitCastOperatorsUsageViaReflectionSupport MSBuild property to true in your project file. " +
					"Note: this feature is not compatible with trimming and with NativeAOT.");
			}
		}
	}
}