[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:
Родитель
88652454e0
Коммит
e2427dfad6
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче