Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Maui.Controls.Build.Tasks;
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Controls.Xaml;
Expand Down Expand Up @@ -190,9 +191,9 @@ public IEnumerable<Instruction> ConvertFromString(string value, ILContext contex
yield break;
}

if (double.TryParse(value, out double radius))
if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out double radius))
{
yield return Instruction.Create(OpCodes.Newobj, module.ImportCtorReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Shapes", "Rectangle"), parameterTypes: null));
yield return Instruction.Create(OpCodes.Newobj, module.ImportCtorReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Shapes", "RoundRectangle"), parameterTypes: null));
yield return Instruction.Create(OpCodes.Dup);

yield return Instruction.Create(OpCodes.Ldc_R8, radius);
Expand Down
336 changes: 210 additions & 126 deletions src/Controls/src/Core/BindablePropertyConverter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#nullable disable
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
Expand All @@ -9,161 +8,246 @@
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Xaml;

namespace Microsoft.Maui.Controls
namespace Microsoft.Maui.Controls;

/// <summary>A TypeConverter that converts strings to <see cref="BindableProperty"/> instances.</summary>
[Xaml.ProvideCompiled("Microsoft.Maui.Controls.XamlC.BindablePropertyConverter")]
public sealed class BindablePropertyConverter : TypeConverter, IExtendedTypeConverter
{
/// <summary>A TypeConverter that converts strings to <see cref="BindableProperty"/> instances.</summary>
[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;

public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
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);
var strValue = value?.ToString() ?? string.Empty;

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);
if (string.IsNullOrWhiteSpace(strValue))
{
return null;
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
if (strValue.IndexOf(":", StringComparison.Ordinal) != -1)
{
MauiLogger<BindablePropertyConverter>.Log(LogLevel.Warning, "Can't resolve properties with xml namespace prefix.");
return null;
}
string[] parts = strValue.Split('.');
if (parts.Length != 2)
{
var strValue = value?.ToString();
MauiLogger<BindablePropertyConverter>.Log(LogLevel.Warning, $"Can't resolve {value}. Accepted syntax is Type.PropertyName.");
return null;
}
Type? type = GetControlType(parts[0]);
if (type == null)
{
MauiLogger<BindablePropertyConverter>.Log(LogLevel.Warning, $"Can't resolve {parts[0]}.");
return null;
}

if (string.IsNullOrWhiteSpace(strValue))
return null;
if (strValue.IndexOf(":", StringComparison.Ordinal) != -1)
{
MauiLogger<BindablePropertyConverter>.Log(LogLevel.Warning, "Can't resolve properties with xml namespace prefix.");
return null;
}
string[] parts = strValue.Split('.');
if (parts.Length != 2)
{
MauiLogger<BindablePropertyConverter>.Log(LogLevel.Warning, $"Can't resolve {value}. Accepted syntax is Type.PropertyName.");
return null;
}
Type type = GetControlType(parts[0]);
return ConvertFrom(type, parts[1], null);
return ConvertFrom(type, parts[1], null);
}

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}";
}

#nullable disable
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;
if (bp == null)
{
throw new XamlParseException($"Can't resolve {name} on {type.Name}", lineinfo);
}

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;
Comment thread
kubaflo marked this conversation as resolved.
}

[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.")]
#nullable enable
static Type? GetControlType(string typeName)
=> Type.GetType("Microsoft.Maui.Controls." + typeName);
#nullable disable

[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

if (parents.Count <= 2)
{
throw new XamlParseException($"Unable to find a TargetType for the Bindable Property. Try prefixing it with the TargetType.", lineInfo);
}

BindableProperty ConvertFrom(Type type, string propertyName, IXmlLineInfo lineinfo)
// VisualStates must be in a VisualStateGroup
if (parents[2] is not VisualStateGroup)
{
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;
throw new XamlParseException($"Expected {nameof(VisualStateGroup)} but found {parents[2]}.", lineInfo);
}

[UnconditionalSuppressMessage("TrimAnalysis", "IL2045:AttributeRemoval",
Justification = "ObsoleteAttribute instances are removed by the trimmer in production builds.")]
static ObsoleteAttribute GetObsoleteAttribute(FieldInfo fieldInfo)
=> fieldInfo.GetCustomAttribute<ObsoleteAttribute>();
if (parents.Count <= 3)
{
throw new XamlParseException($"Unable to find a TargetType for the Bindable Property. Try prefixing it with the TargetType.", lineInfo);
}

[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);
// Are these Visual States directly on a VisualElement?
if (parents[3] is VisualElement vsTarget)
{
return vsTarget.GetType();
}

[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);
if (parents[3] is not VisualStateGroupList)
{
throw new XamlParseException($"Expected {nameof(VisualStateGroupList)} but found {parents[3]}.", lineInfo);
}

Type FindTypeForVisualState(IProvideParentValues parentValueProvider, IXmlLineInfo lineInfo)
if (parents.Count <= 4)
{
var parents = parentValueProvider.ParentObjects.ToList();
throw new XamlParseException($"Unable to find a TargetType for the Bindable Property. Try prefixing it with the TargetType.", lineInfo);
}

// 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
if (parents[4] is VisualElement veTarget)
{
return veTarget.GetType();
}

// VisualStates must be in a VisualStateGroup
if (parents[2] is not VisualStateGroup)
throw new XamlParseException($"Expected {nameof(VisualStateGroup)} but found {parents[2]}.", lineInfo);
if (parents[4] is not Setter)
{
throw new XamlParseException($"Expected {nameof(Setter)} but found {parents[4]}.", lineInfo);
}

// Are these Visual States directly on a VisualElement?
if (parents[3] is VisualElement vsTarget)
return vsTarget.GetType();
if (parents.Count <= 5)
{
throw new XamlParseException($"Unable to find a TargetType for the Bindable Property. Try prefixing it with the TargetType.", lineInfo);
}

if (parents[3] is not VisualStateGroupList)
throw new XamlParseException($"Expected {nameof(VisualStateGroupList)} but found {parents[3]}.", lineInfo);
if (parents[5] is TriggerBase trigger)
{
return trigger.TargetType;
}

if (parents[4] is VisualElement veTarget)
return veTarget.GetType();
// These must be part of a Style; verify that
if (parents[5] is Style style)
{
return style.TargetType;
}

if (parents[4] is not Setter)
throw new XamlParseException($"Expected {nameof(Setter)} but found {parents[4]}.", lineInfo);
throw new XamlParseException($"Unable to find a TargetType for the Bindable Property. Try prefixing it with the TargetType.", 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;
object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}

throw new XamlParseException($"Unable to find a TragetType for the Bindable Property. Try prefixing it with the TargetType.", lineInfo);
if (serviceProvider == null)
{
return null;
}

if (!(serviceProvider.GetService(typeof(IXamlTypeResolver)) is IXamlTypeResolver typeResolver))
{
return null;
}

public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
IXmlLineInfo lineinfo = null;
if (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider xmlLineInfoProvider)
{
if (value is not BindableProperty bp)
throw new NotSupportedException();
return $"{bp.DeclaringType.Name}.{bp.PropertyName}";
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);
}

}
Loading
Loading