Browse Source

Compiled Bindings: Fix unpredictable binding to method selection (#21617)

* Fix unpredictable binding to method selection (compiled bindings)

* Adjust tests

* Add more test cases
pull/21624/head
Max Katz 3 months ago
committed by GitHub
parent
commit
6d950d8e15
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 21
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  2. 21
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlTrampolineBuilder.cs
  3. 34
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

21
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@ -210,9 +210,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
{ {
nodes.Add(new XamlIlClrPropertyPathElementNode(clrProperty, propName.AcceptsNull)); nodes.Add(new XamlIlClrPropertyPathElementNode(clrProperty, propName.AcceptsNull));
} }
else if (GetAllDefinedMethods(targetType).FirstOrDefault(m => m.Name == propName.PropertyName) is IXamlMethod method) else if (GetAllDefinedMethods(targetType)
.Where(p => p.Name == propName.PropertyName)
.OrderByDescending(m => m.Parameters.Count)
.ToArray()
is { Length: > 0 } methodCandidates)
{ {
nodes.Add(new XamlIlClrMethodPathElementNode(method, context.Configuration.WellKnownTypes.Delegate, propName.AcceptsNull)); var objType = context.Configuration.WellKnownTypes.Object;
var candidate = methodCandidates
.FirstOrDefault(m => m.Parameters.Count == 0
|| (m.Parameters.Count == 1 &&
m.Parameters[0].Equals(objType)));
if (candidate is null)
{
throw new XamlX.XamlTransformException(
$"Unable to resolve method of name '{propName.PropertyName}' on type '{targetType}'." +
$"Expected method with no parameters or a single object overload.",
lineInfo);
}
nodes.Add(new XamlIlClrMethodPathElementNode(candidate, context.Configuration.WellKnownTypes.Delegate, propName.AcceptsNull));
} }
else else
{ {

21
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlTrampolineBuilder.cs

@ -48,24 +48,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
} }
if (executeMethod.Parameters.Count != 0) if (executeMethod.Parameters.Count != 0)
{ {
Debug.Assert(executeMethod.Parameters.Count == 1); Debug.Assert(executeMethod.Parameters.Count == 1
if (executeMethod.Parameters[0] != context.Configuration.WellKnownTypes.Object) && executeMethod.Parameters[0] == context.Configuration.WellKnownTypes.Object);
{ gen.Ldarg(1);
var convertedValue = gen.DefineLocal(context.Configuration.WellKnownTypes.Object);
gen.Ldtype(executeMethod.Parameters[0])
.Ldarg(1)
.EmitCall(context.Configuration.WellKnownTypes.CultureInfo.GetMethod(m => m.Name == "get_CurrentCulture"))
.Ldloca(convertedValue)
.EmitCall(
context.GetAvaloniaTypes().TypeUtilities.GetMethod(m => m.Name == "TryConvert"),
swallowResult: true)
.Ldloc(convertedValue)
.Unbox_Any(executeMethod.Parameters[0]);
}
else
{
gen.Ldarg(1);
}
} }
gen.EmitCall(executeMethod, swallowResult: true); gen.EmitCall(executeMethod, swallowResult: true);
gen.Ret(); gen.Ret();

34
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -1863,8 +1863,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
<StackPanel> <StackPanel>
<ContentControl Content='{CompiledBinding Action}' Name='action' /> <ContentControl Content='{CompiledBinding Action}' Name='action' />
<ContentControl Content='{CompiledBinding Func}' Name='func' /> <ContentControl Content='{CompiledBinding Func}' Name='func' />
<ContentControl Content='{CompiledBinding Action16}' Name='action16' /> <ContentControl Content='{CompiledBinding Func2}' Name='func2' />
<ContentControl Content='{CompiledBinding Func16}' Name='func16' />
<ContentControl Content='{CompiledBinding CustomDelegateTypeVoid}' Name='customvoid' /> <ContentControl Content='{CompiledBinding CustomDelegateTypeVoid}' Name='customvoid' />
<ContentControl Content='{CompiledBinding CustomDelegateTypeInt}' Name='customint' /> <ContentControl Content='{CompiledBinding CustomDelegateTypeInt}' Name='customint' />
</StackPanel> </StackPanel>
@ -1873,9 +1872,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
window.DataContext = new MethodDataContext(); window.DataContext = new MethodDataContext();
Assert.IsAssignableFrom(typeof(Action), window.GetControl<ContentControl>("action").Content); Assert.IsAssignableFrom(typeof(Action), window.GetControl<ContentControl>("action").Content);
Assert.IsAssignableFrom(typeof(Func<int>), window.GetControl<ContentControl>("func").Content); Assert.IsAssignableFrom(typeof(Func<object>), window.GetControl<ContentControl>("func").Content);
Assert.IsAssignableFrom(typeof(Action<int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int>), window.GetControl<ContentControl>("action16").Content); Assert.IsAssignableFrom(typeof(Func<object, object>), window.GetControl<ContentControl>("func2").Content);
Assert.IsAssignableFrom(typeof(Func<int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int>), window.GetControl<ContentControl>("func16").Content);
Assert.True(typeof(Delegate).IsAssignableFrom(window.GetControl<ContentControl>("customvoid").Content!.GetType())); Assert.True(typeof(Delegate).IsAssignableFrom(window.GetControl<ContentControl>("customvoid").Content!.GetType()));
Assert.True(typeof(Delegate).IsAssignableFrom(window.GetControl<ContentControl>("customint").Content!.GetType())); Assert.True(typeof(Delegate).IsAssignableFrom(window.GetControl<ContentControl>("customint").Content!.GetType()));
} }
@ -1906,17 +1904,20 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
} }
[Fact] [Theory]
public void Binding_Method_With_Parameter_To_Command_Works() [InlineData("5")]
[InlineData("hello")]
[InlineData(null)]
public void Binding_Method_With_Parameter_To_Command_Works(string? parameter)
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var xaml = @" var xaml = $@"
<Window xmlns='https://github.com/avaloniaui' <Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:MethodAsCommandDataContext'> x:DataType='local:MethodAsCommandDataContext'>
<Button Name='button' Command='{CompiledBinding Method1}' CommandParameter='5'/> <Button Name='button' Command='{{CompiledBinding Method1}}' CommandParameter='{ parameter ?? "{x:Null}" }'/>
</Window>"; </Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.GetControl<Button>("button"); var button = window.GetControl<Button>("button");
@ -1927,7 +1928,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.NotNull(button.Command); Assert.NotNull(button.Command);
PerformClick(button); PerformClick(button);
Assert.Equal("Called 5", vm.Value); Assert.Equal("Called " + parameter, vm.Value);
} }
} }
@ -2533,12 +2534,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{ {
public void Action() { } public void Action() { }
public int Func() => 1; public object Func() => 1;
public object Func2(object i) => i;
public void Action16(int i, int i2, int i3, int i4, int i5, int i6, int i7, int i8, int i9, int i10, int i11, int i12, int i13, int i14, int i15, int i16) { } public void CustomDelegateTypeVoid(object i) { }
public int Func16(int i, int i2, int i3, int i4, int i5, int i6, int i7, int i8, int i9, int i10, int i11, int i12, int i13, int i14, int i15, int i16) => i; public object CustomDelegateTypeInt(object i) => i;
public void CustomDelegateTypeVoid(int i, int i2, int i3, int i4, int i5, int i6, int i7, int i8, int i9, int i10, int i11, int i12, int i13, int i14, int i15, int i16, int i17) { }
public int CustomDelegateTypeInt(int i, int i2, int i3, int i4, int i5, int i6, int i7, int i8, int i9, int i10, int i11, int i12, int i13, int i14, int i15, int i16, int i17) => i;
} }
public class MethodAsCommandDataContext : INotifyPropertyChanged public class MethodAsCommandDataContext : INotifyPropertyChanged
@ -2546,7 +2546,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
public string Method() => Value = "Called"; public string Method() => Value = "Called";
public string Method1(int i) => Value = $"Called {i}"; public string Method1() => Value = "Called";
public string Method1(int i) => throw new InvalidOperationException("Binding to method with typed parameters is not supported");
public string Method1(object i) => Value = $"Called {i}";
public string Method2(int i, int j) => Value = $"Called {i},{j}"; public string Method2(int i, int j) => Value = $"Called {i},{j}";
public string Value { get; private set; } = "Not called"; public string Value { get; private set; } = "Not called";

Loading…
Cancel
Save