[XC] Fallback to reflection-based bindings for bindings with "invalid" path (#24238)

* Add test

* Skip binding compilation when path cannot be matched to the data type

* Update outdated tests

* Do not treat XC0045 as error in Xaml.UnitTests
This commit is contained in:
Šimon Rozsíval 2024-08-27 16:37:13 +02:00 коммит произвёл GitHub
Родитель 88652454e0
Коммит e2427dfad6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
9 изменённых файлов: 115 добавлений и 81 удалений

Просмотреть файл

@ -300,8 +300,11 @@ namespace Microsoft.Maui.Controls.Build.Tasks
if (bindingExtensionType.HasValue)
{
foreach (var instruction in CompileBindingPath(node, context, vardefref.VariableDefinition, bindingExtensionType.Value, isStandaloneBinding: bpRef is null))
yield return instruction;
if (TryCompileBindingPath(node, context, vardefref.VariableDefinition, bindingExtensionType.Value, isStandaloneBinding: bpRef is null, out var instructions))
{
foreach (var instruction in instructions)
yield return instruction;
}
}
var markExt = markupExtension.ResolveCached(context.Cache);
@ -390,8 +393,10 @@ namespace Microsoft.Maui.Controls.Build.Tasks
}
//Once we get compiled IValueProvider, this will move to the BindingExpression
static IEnumerable<Instruction> CompileBindingPath(ElementNode node, ILContext context, VariableDefinition bindingExt, (string, string, string) bindingExtensionType, bool isStandaloneBinding)
static bool TryCompileBindingPath(ElementNode node, ILContext context, VariableDefinition bindingExt, (string, string, string) bindingExtensionType, bool isStandaloneBinding, out IEnumerable<Instruction> instructions)
{
instructions = null;
//TODO support casting operators
var module = context.Module;
@ -444,7 +449,7 @@ namespace Microsoft.Maui.Controls.Build.Tasks
{
context.LoggingHelper.LogWarningOrError(BuildExceptionCode.BindingWithoutDataType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null);
yield break;
return false;
}
if (xDataTypeIsInOuterScope)
@ -458,7 +463,7 @@ namespace Microsoft.Maui.Controls.Build.Tasks
&& enode.XmlType.Name == nameof(Microsoft.Maui.Controls.Xaml.NullExtension))
{
context.LoggingHelper.LogWarningOrError(BuildExceptionCode.BindingWithNullDataType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null);
yield break;
return false;
}
string dataType = (dataTypeNode as ValueNode)?.Value as string;
@ -479,54 +484,64 @@ namespace Microsoft.Maui.Controls.Build.Tasks
var tSourceRef = dtXType.GetTypeReference(context.Cache, module, (IXmlLineInfo)node);
if (tSourceRef == null)
yield break; //throw
return false; //throw
var properties = ParsePath(context, path, tSourceRef, node as IXmlLineInfo, module);
TypeReference tPropertyRef = tSourceRef;
if (properties != null && properties.Count > 0)
if (!TryParsePath(context, path, tSourceRef, node as IXmlLineInfo, module, out var properties))
{
var lastProp = properties[properties.Count - 1];
if (lastProp.property != null)
tPropertyRef = lastProp.property.PropertyType.ResolveGenericParameters(lastProp.propDeclTypeRef);
else //array type
tPropertyRef = lastProp.propDeclTypeRef.ResolveCached(context.Cache);
return false;
}
tPropertyRef = module.ImportReference(tPropertyRef);
var valuetupleRef = context.Module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "ValueTuple`2")).MakeGenericInstanceType(new[] { tPropertyRef, module.TypeSystem.Boolean }));
var funcRef = module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "Func`2")).MakeGenericInstanceType(new[] { tSourceRef, valuetupleRef }));
var actionRef = module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "Action`2")).MakeGenericInstanceType(new[] { tSourceRef, tPropertyRef }));
var funcObjRef = module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "Func`2")).MakeGenericInstanceType(new[] { tSourceRef, module.TypeSystem.Object }));
var tupleRef = module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "Tuple`2")).MakeGenericInstanceType(new[] { funcObjRef, module.TypeSystem.String }));
var typedBindingRef = module.ImportReference(module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Internals", "TypedBinding`2")).MakeGenericInstanceType(new[] { tSourceRef, tPropertyRef }));
//FIXME: make sure the non-deprecated one is used
var ctorInfo = module.ImportReference(typedBindingRef.ResolveCached(context.Cache).Methods.FirstOrDefault(md =>
md.IsConstructor
&& !md.IsStatic
&& md.Parameters.Count == 3
&& !md.HasCustomAttributes(module.ImportReference(context.Cache, ("mscorlib", "System", "ObsoleteAttribute")))));
var ctorinforef = ctorInfo.MakeGeneric(typedBindingRef, funcRef, actionRef, tupleRef);
instructions = GenerateInstructions();
return true;
foreach (var instruction in bindingExt.LoadAs(context.Cache, module.GetTypeDefinition(context.Cache, bindingExtensionType), module))
yield return instruction;
foreach (var instruction in CompiledBindingGetGetter(tSourceRef, tPropertyRef, properties, node, context))
yield return instruction;
if (declaredmode != BindingMode.OneTime && declaredmode != BindingMode.OneWay)
{ //if the mode is explicitly 1w, or 1t, no need for setters
foreach (var instruction in CompiledBindingGetSetter(tSourceRef, tPropertyRef, properties, node, context))
IEnumerable<Instruction> GenerateInstructions()
{
TypeReference tPropertyRef = tSourceRef;
if (properties != null && properties.Count > 0)
{
var lastProp = properties[properties.Count - 1];
if (lastProp.property != null)
tPropertyRef = lastProp.property.PropertyType.ResolveGenericParameters(lastProp.propDeclTypeRef);
else //array type
tPropertyRef = lastProp.propDeclTypeRef.ResolveCached(context.Cache);
}
tPropertyRef = module.ImportReference(tPropertyRef);
var valuetupleRef = context.Module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "ValueTuple`2")).MakeGenericInstanceType(new[] { tPropertyRef, module.TypeSystem.Boolean }));
var funcRef = module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "Func`2")).MakeGenericInstanceType(new[] { tSourceRef, valuetupleRef }));
var actionRef = module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "Action`2")).MakeGenericInstanceType(new[] { tSourceRef, tPropertyRef }));
var funcObjRef = module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "Func`2")).MakeGenericInstanceType(new[] { tSourceRef, module.TypeSystem.Object }));
var tupleRef = module.ImportReference(module.ImportReference(context.Cache, ("mscorlib", "System", "Tuple`2")).MakeGenericInstanceType(new[] { funcObjRef, module.TypeSystem.String }));
var typedBindingRef = module.ImportReference(module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Internals", "TypedBinding`2")).MakeGenericInstanceType(new[] { tSourceRef, tPropertyRef }));
//FIXME: make sure the non-deprecated one is used
var ctorInfo = module.ImportReference(typedBindingRef.ResolveCached(context.Cache).Methods.FirstOrDefault(md =>
md.IsConstructor
&& !md.IsStatic
&& md.Parameters.Count == 3
&& !md.HasCustomAttributes(module.ImportReference(context.Cache, ("mscorlib", "System", "ObsoleteAttribute")))));
var ctorinforef = ctorInfo.MakeGeneric(typedBindingRef, funcRef, actionRef, tupleRef);
foreach (var instruction in bindingExt.LoadAs(context.Cache, module.GetTypeDefinition(context.Cache, bindingExtensionType), module))
yield return instruction;
}
else
yield return Create(Ldnull);
if (declaredmode != BindingMode.OneTime)
{ //if the mode is explicitly 1t, no need for handlers
foreach (var instruction in CompiledBindingGetHandlers(tSourceRef, tPropertyRef, properties, node, context))
foreach (var instruction in CompiledBindingGetGetter(tSourceRef, tPropertyRef, properties, node, context))
yield return instruction;
if (declaredmode != BindingMode.OneTime && declaredmode != BindingMode.OneWay)
{ //if the mode is explicitly 1w, or 1t, no need for setters
foreach (var instruction in CompiledBindingGetSetter(tSourceRef, tPropertyRef, properties, node, context))
yield return instruction;
}
else
yield return Create(Ldnull);
if (declaredmode != BindingMode.OneTime)
{ //if the mode is explicitly 1t, no need for handlers
foreach (var instruction in CompiledBindingGetHandlers(tSourceRef, tPropertyRef, properties, node, context))
yield return instruction;
}
else
yield return Create(Ldnull);
yield return Create(Newobj, module.ImportReference(ctorinforef));
yield return Create(Callvirt, module.ImportPropertySetterReference(context.Cache, bindingExtensionType, propertyName: "TypedBinding"));
}
else
yield return Create(Ldnull);
yield return Create(Newobj, module.ImportReference(ctorinforef));
yield return Create(Callvirt, module.ImportPropertySetterReference(context.Cache, bindingExtensionType, propertyName: "TypedBinding"));
static IElementNode GetParent(IElementNode node)
{
@ -548,10 +563,13 @@ namespace Microsoft.Maui.Controls.Build.Tasks
}
}
static IList<(PropertyDefinition property, TypeReference propDeclTypeRef, string indexArg)> ParsePath(ILContext context, string path, TypeReference tSourceRef, IXmlLineInfo lineInfo, ModuleDefinition module)
static bool TryParsePath(ILContext context, string path, TypeReference tSourceRef, IXmlLineInfo lineInfo, ModuleDefinition module, out IList<(PropertyDefinition property, TypeReference propDeclTypeRef, string indexArg)> pathProperties)
{
pathProperties = null;
if (string.IsNullOrWhiteSpace(path))
return null;
return true;
path = path.Trim(' ', '.'); //trim leading or trailing dots
var parts = path.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
var properties = new List<(PropertyDefinition property, TypeReference propDeclTypeRef, string indexArg)>();
@ -582,8 +600,13 @@ namespace Microsoft.Maui.Controls.Build.Tasks
if (p.Length > 0)
{
var property = previousPartTypeRef.GetProperty(context.Cache, pd => pd.Name == p && pd.GetMethod != null && pd.GetMethod.IsPublic && !pd.GetMethod.IsStatic, out var propDeclTypeRef)
?? throw new BuildException(BuildExceptionCode.BindingPropertyNotFound, lineInfo, null, p, previousPartTypeRef);
var property = previousPartTypeRef.GetProperty(context.Cache, pd => pd.Name == p && pd.GetMethod != null && pd.GetMethod.IsPublic && !pd.GetMethod.IsStatic, out var propDeclTypeRef);
if (property is null)
{
context.LoggingHelper.LogWarningOrError(BuildExceptionCode.BindingPropertyNotFound, context.XamlFilePath, lineInfo.LineNumber, lineInfo.LinePosition, 0, 0, p, previousPartTypeRef);
return false;
}
properties.Add((property, propDeclTypeRef, null));
previousPartTypeRef = property.PropertyType.ResolveGenericParameters(propDeclTypeRef);
}
@ -623,7 +646,8 @@ namespace Microsoft.Maui.Controls.Build.Tasks
}
}
return properties;
pathProperties = properties;
return true;
}
static IEnumerable<Instruction> DigProperties(IEnumerable<(PropertyDefinition property, TypeReference propDeclTypeRef, string indexArg)> properties, Dictionary<TypeReference, VariableDefinition> locs, Func<Instruction> fallback, IXmlLineInfo lineInfo, ModuleDefinition module)

Просмотреть файл

@ -5,8 +5,10 @@
xmlns:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:cmp="clr-namespace:Microsoft.Maui.Controls.Compatibility;assembly=Microsoft.Maui.Controls"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.BindingsCompiler" >
<cmp:StackLayout>
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.BindingsCompiler"
x:Name="page"
x:DataType="local:GlobalViewModel">
<cmp:StackLayout x:Name="stack" x:DataType="{x:Null}">
<cmp:StackLayout x:DataType="local:MockViewModel">
<Label Text="{Binding Text}" x:Name="label0" />
<Label Text="{Binding Path=Text}" x:Name="label1" />
@ -23,6 +25,7 @@
<Label Text="{Binding StructModel.Model.Text, Mode=TwoWay}" x:Name="label10" />
<Label Text="Text for label12" x:Name="label11" />
<Label Text="{Binding Text, x:DataType=Label, Source={x:Reference label11}}" x:Name="label12" />
<Label Text="{Binding BindingContext.GlobalText, Source={x:Reference page}, x:DataType=ContentPage}" x:Name="label13" />
<Picker
ItemsSource="{Binding Items}"

Просмотреть файл

@ -56,8 +56,9 @@ namespace Microsoft.Maui.Controls.Xaml.UnitTests
var layout = new BindingsCompiler(useCompiledXaml)
{
BindingContext = vm
BindingContext = new GlobalViewModel(),
};
layout.stack.BindingContext = vm;
layout.label6.BindingContext = new MockStructViewModel
{
Model = new MockViewModel
@ -121,11 +122,14 @@ namespace Microsoft.Maui.Controls.Xaml.UnitTests
}
//testing invalid bindingcontext type
layout.BindingContext = new object();
layout.stack.BindingContext = new object();
Assert.AreEqual(null, layout.label0.Text);
//testing source
Assert.That(layout.label12.Text, Is.EqualTo("Text for label12"));
//testing binding with path that cannot be statically compiled (we don't support casts in the Path)
Assert.That(layout.label13.Text, Is.EqualTo("Global Text"));
}
[Test]
@ -148,6 +152,11 @@ namespace Microsoft.Maui.Controls.Xaml.UnitTests
public MockViewModel Model { get; set; }
}
class GlobalViewModel
{
public string GlobalText { get; set; } = "Global Text";
}
class MockViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

Просмотреть файл

@ -6,7 +6,7 @@
<AssemblyName>Microsoft.Maui.Controls.Xaml.UnitTests</AssemblyName>
<WarningLevel>4</WarningLevel>
<NoWarn>$(NoWarn);0672;0219;0414;CS0436;CS0618</NoWarn>
<WarningsNotAsErrors>$(WarningsNotAsErrors);XC0618;XC0022,XC0023</WarningsNotAsErrors>
<WarningsNotAsErrors>$(WarningsNotAsErrors);XC0618;XC0022,XC0023;XC0045</WarningsNotAsErrors>
<IsPackable>false</IsPackable>
<DisableMSBuildAssemblyCopyCheck>true</DisableMSBuildAssemblyCopyCheck>
</PropertyGroup>

Просмотреть файл

@ -3,5 +3,5 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Gh2517"
x:DataType="Label">
<Label Text="{Binding MissingProperty}" />
<Label Text="{Binding MissingProperty}" x:Name="Label" />
</ContentPage>

Просмотреть файл

@ -7,7 +7,7 @@ using NUnit.Framework;
namespace Microsoft.Maui.Controls.Xaml.UnitTests
{
[XamlCompilation(XamlCompilationOptions.Skip)]
// related to https://github.com/dotnet/maui/issues/23711
public partial class Gh2517 : ContentPage
{
public Gh2517()
@ -24,10 +24,15 @@ namespace Microsoft.Maui.Controls.Xaml.UnitTests
class Tests
{
[TestCase(true)]
public void ErrorOnMissingBindingTarget(bool useCompiledXaml)
public void BindingWithInvalidPathIsNotCompiled(bool useCompiledXaml)
{
if (useCompiledXaml)
Assert.Throws<BuildException>(() => MockCompiler.Compile(typeof(Gh2517)));
MockCompiler.Compile(typeof(Gh2517));
var view = new Gh2517(useCompiledXaml);
var binding = view.Label.GetContext(Label.TextProperty).Bindings.GetValue();
Assert.That(binding, Is.TypeOf<Binding>());
}
}
}

Просмотреть файл

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Gh3606" x:Name="page">
<StackLayout x:DataType="x:String">
<Label Text="{Binding Content, Source={x:Reference page}}" />
<Label Text="{Binding Content, Source={x:Reference page}}" x:Name="Label" />
</StackLayout>
</ContentPage>

Просмотреть файл

@ -8,7 +8,7 @@ using NUnit.Framework;
namespace Microsoft.Maui.Controls.Xaml.UnitTests
{
[XamlCompilation(XamlCompilationOptions.Skip)]
// related to https://github.com/dotnet/maui/issues/23711
public partial class Gh3606 : ContentPage
{
public Gh3606()
@ -30,17 +30,15 @@ namespace Microsoft.Maui.Controls.Xaml.UnitTests
[TestCase(true)]
[TestCase(false)]
public void BindingsWithSourceAreCompiled(bool useCompiledXaml)
public void BindingsWithSourceAndInvalidPathAreNotCompiled(bool useCompiledXaml)
{
if (useCompiledXaml)
{
// The XAML file contains a mismatch between the source the x:DataType attribute so the compilation of the binding will fail
Assert.Throws(new BuildExceptionConstraint(4, 16), () => MockCompiler.Compile(typeof(Gh3606)));
}
else
{
Assert.DoesNotThrow(() => new Gh3606(useCompiledXaml: false));
}
MockCompiler.Compile(typeof(Gh3606));
var view = new Gh3606(useCompiledXaml);
var binding = view.Label.GetContext(Label.TextProperty).Bindings.GetValue();
Assert.That(binding, Is.TypeOf<Binding>());
}
}
}

Просмотреть файл

@ -15,7 +15,6 @@ using NUnit.Framework;
namespace Microsoft.Maui.Controls.Xaml.UnitTests;
[XamlCompilation(XamlCompilationOptions.Skip)]
public partial class Maui20768
{
public Maui20768()
@ -43,16 +42,12 @@ public partial class Maui20768
[Test]
public void BindingsDoNotResolveStaticProperties([Values(false, true)] bool useCompiledXaml)
{
if (useCompiledXaml)
{
Assert.Throws(new BuildExceptionConstraint(6, 32), () => MockCompiler.Compile(typeof(Maui20768)));
}
else
{
var page = new Maui20768(useCompiledXaml);
page.TitleLabel.BindingContext = new ViewModel20768();
Assert.Null(page.TitleLabel.Text);
}
if (useCompiledXaml)
MockCompiler.Compile(typeof(Maui20768));
var page = new Maui20768(useCompiledXaml);
page.TitleLabel.BindingContext = new ViewModel20768();
Assert.Null(page.TitleLabel.Text);
}
}
}