File: BindablePropertyConverter.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Xml;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Xaml;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../docs/Microsoft.Maui.Controls/BindablePropertyConverter.xml" path="Type[@FullName='Microsoft.Maui.Controls.BindablePropertyConverter']/Docs/*" />
	[Xaml.ProvideCompiled("Microsoft.Maui.Controls.XamlC.BindablePropertyConverter")]
	public sealed class BindablePropertyConverter : TypeConverter, IExtendedTypeConverter
	{
		public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
			=> sourceType == typeof(string);
 
		public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
			=> true;
 
		object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
		{
			if (string.IsNullOrWhiteSpace(value))
				return null;
			if (serviceProvider == null)
				return null;
			if (!(serviceProvider.GetService(typeof(IXamlTypeResolver)) is IXamlTypeResolver typeResolver))
				return null;
			IXmlLineInfo lineinfo = null;
			if (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider xmlLineInfoProvider)
				lineinfo = xmlLineInfoProvider.XmlLineInfo;
			string[] parts = value.Split('.');
			Type type = null;
			if (parts.Length == 1)
			{
				if (!(serviceProvider.GetService(typeof(IProvideValueTarget)) is IProvideParentValues parentValuesProvider))
				{
					string msg = string.Format("Can't resolve {0}", parts[0]);
					throw new XamlParseException(msg, lineinfo);
				}
				object parent = parentValuesProvider.ParentObjects.Skip(1).FirstOrDefault();
				if (parentValuesProvider.TargetObject is Setter)
				{
					if (parent is Style style)
						type = style.TargetType;
					else if (parent is TriggerBase triggerBase)
						type = triggerBase.TargetType;
					else if (parent is VisualState visualState)
						type = FindTypeForVisualState(parentValuesProvider, lineinfo);
				}
				else if (parentValuesProvider.TargetObject is Trigger)
					type = (parentValuesProvider.TargetObject as Trigger).TargetType;
				else if (parentValuesProvider.TargetObject is PropertyCondition && parent is TriggerBase)
					type = (parent as TriggerBase).TargetType;
 
				if (type == null)
					throw new XamlParseException($"Can't resolve {parts[0]}", lineinfo);
 
				return ConvertFrom(type, parts[0], lineinfo);
			}
			if (parts.Length == 2)
			{
				if (!typeResolver.TryResolve(parts[0], out type))
				{
					string msg = string.Format("Can't resolve {0}", parts[0]);
					throw new XamlParseException(msg, lineinfo);
				}
				return ConvertFrom(type, parts[1], lineinfo);
			}
			throw new XamlParseException($"Can't resolve {value}. Syntax is [[prefix:]Type.]PropertyName.", lineinfo);
		}
 
		public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
		{
			var strValue = value?.ToString();
 
			if (string.IsNullOrWhiteSpace(strValue))
				return null;
			if (strValue.IndexOf(":", StringComparison.Ordinal) != -1)
			{
				Application.Current?.FindMauiContext()?.CreateLogger<BindablePropertyConverter>()?.LogWarning("Can't resolve properties with xml namespace prefix.");
				return null;
			}
			string[] parts = strValue.Split('.');
			if (parts.Length != 2)
			{
				Application.Current?.FindMauiContext()?.CreateLogger<BindablePropertyConverter>()?.LogWarning($"Can't resolve {value}. Accepted syntax is Type.PropertyName.");
				return null;
			}
			Type type = GetControlType(parts[0]);
			return ConvertFrom(type, parts[1], null);
		}
 
		BindableProperty ConvertFrom(Type type, string propertyName, IXmlLineInfo lineinfo)
		{
			var name = propertyName + "Property";
			FieldInfo bpinfo = GetPropertyField(type, name);
			if (bpinfo == null || bpinfo.FieldType != typeof(BindableProperty))
				throw new XamlParseException($"Can't resolve {name} on {type.Name}", lineinfo);
			var bp = bpinfo.GetValue(null) as BindableProperty;
			var isObsolete = GetObsoleteAttribute(bpinfo) != null;
			if (bp.PropertyName != propertyName && !isObsolete)
				throw new XamlParseException($"The PropertyName of {type.Name}.{name} is not {propertyName}", lineinfo);
			return bp;
		}
 
		[UnconditionalSuppressMessage("TrimAnalysis", "IL2045:AttributeRemoval",
			Justification = "ObsoleteAttribute instances are removed by the trimmer in production builds.")]
		static ObsoleteAttribute GetObsoleteAttribute(FieldInfo fieldInfo)
			=> fieldInfo.GetCustomAttribute<ObsoleteAttribute>();
 
		[UnconditionalSuppressMessage("TrimAnalysis", "IL2057:TypeGetType",
			Justification = "The converter is only used when parsing XAML at runtime. The developer will receive a warning " +
				"saying that parsing XAML at runtime may not work as expected when trimming.")]
		static Type GetControlType(string typeName)
			=> Type.GetType("Microsoft.Maui.Controls." + typeName);
 
		[UnconditionalSuppressMessage("TrimAnalysis", "IL2070:UnrecognizedReflectionPattern",
			Justification = "The converter is only used when parsing XAML at runtime. The developer will receive a warning " +
				"saying that parsing XAML at runtime may not work as expected when trimming.")]
		static FieldInfo GetPropertyField(Type type, string fieldName)
			=> type.GetField(fieldName, BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
 
		Type FindTypeForVisualState(IProvideParentValues parentValueProvider, IXmlLineInfo lineInfo)
		{
			var parents = parentValueProvider.ParentObjects.ToList();
 
			// Skip 0; we would not be making this check if TargetObject were not a Setter
			// Skip 1; we would not be making this check if the immediate parent were not a VisualState
 
			// VisualStates must be in a VisualStateGroup
			if (parents[2] is not VisualStateGroup)
				throw new XamlParseException($"Expected {nameof(VisualStateGroup)} but found {parents[2]}.", lineInfo);
 
			// Are these Visual States directly on a VisualElement?
			if (parents[3] is VisualElement vsTarget)
				return vsTarget.GetType();
 
			if (parents[3] is not VisualStateGroupList)
				throw new XamlParseException($"Expected {nameof(VisualStateGroupList)} but found {parents[3]}.", lineInfo);
 
			if (parents[4] is VisualElement veTarget)
				return veTarget.GetType();
 
			if (parents[4] is not Setter)
				throw new XamlParseException($"Expected {nameof(Setter)} but found {parents[4]}.", lineInfo);
 
			if (parents[5] is TriggerBase trigger)
				return trigger.TargetType;
 
			// These must be part of a Style; verify that 
			if (parents[5] is Style style)
				return style.TargetType;
 
			throw new XamlParseException($"Unable to find a TragetType for the Bindable Property. Try prefixing it with the TargetType.", lineInfo);
 
		}
 
		public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
		{
			if (value is not BindableProperty bp)
				throw new NotSupportedException();
			return $"{bp.DeclaringType.Name}.{bp.PropertyName}";
		}
	}
}