Browse Source

Allow single parameter with any type when binding to method (#21867)

* Allow single parameter with any type when binding to method

* Port method binding logic to ReflectionBinding

* Don't depend on the order of methods

* Handle overrides properly

* Fix nullability warning
pull/21880/head
Julien Lebosquain 2 months ago
committed by GitHub
parent
commit
5495737014
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 124
      src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs
  2. 97
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  3. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlTrampolineBuilder.cs
  4. 225
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs
  5. 239
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

124
src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
@ -10,10 +11,9 @@ namespace Avalonia.Data.Core.Plugins
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
internal class ReflectionMethodAccessorPlugin : IPropertyAccessorPlugin
{
private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup =
new Dictionary<(Type, string), MethodInfo?>();
private readonly Dictionary<(Type, string), MethodLookupResult> _methodLookup = new();
public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null;
public bool Match(object obj, string methodName) => GetMethod(obj.GetType(), methodName).IsMatch;
public IPropertyAccessor? Start(WeakReference<object?> reference, string methodName)
{
@ -23,63 +23,133 @@ namespace Avalonia.Data.Core.Plugins
if (!reference.TryGetTarget(out var instance) || instance is null)
return null;
var method = GetFirstMethodWithName(instance.GetType(), methodName);
var result = GetMethod(instance.GetType(), methodName);
if (method is not null)
if (result.Method is { } method)
{
return new Accessor(reference, method);
}
else
{
var message = $"Could not find CLR method '{methodName}' on '{instance}'";
var exception = new MissingMemberException(message);
Exception exception = result.Error is { } error
? new AmbiguousMatchException(error)
: new MissingMemberException($"Could not find CLR method '{methodName}' on '{instance}'");
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
}
private MethodInfo? GetFirstMethodWithName(
private MethodLookupResult GetMethod(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName)
{
var key = (type, methodName);
if (!_methodLookup.TryGetValue(key, out var methodInfo))
if (!_methodLookup.TryGetValue(key, out var result))
{
methodInfo = TryFindAndCacheMethod(type, methodName);
result = FindBestCommandMethod(type, methodName);
_methodLookup.Add(key, result);
}
return methodInfo;
return result;
}
private MethodInfo? TryFindAndCacheMethod(
/// <summary>
/// Finds the method named <paramref name="methodName"/> which can be bound to a command.
/// </summary>
/// <remarks>
/// Priority:
/// 1. One parameter method
/// 1a. Object parameter (amongst several overloads)
/// 1b. Single method with one parameter
/// 2. Zero parameters method
/// </remarks>
private static MethodLookupResult FindBestCommandMethod(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName)
{
MethodInfo? found = null;
const BindingFlags bindingFlags =
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
var methods = type.GetMethods(bindingFlags);
List<MethodInfo>? candidates = null;
foreach (var methodInfo in methods)
foreach (var methodInfo in type.GetMethods(bindingFlags))
{
if (methodInfo.Name == methodName)
(candidates ??= []).Add(methodInfo);
}
if (candidates is null)
return default;
MethodInfo? zeroParamCandidate = null;
Dictionary<Type, MethodInfo>? oneParamCandidates = null;
foreach (var candidate in candidates)
{
var parameters = candidate.GetParameters();
switch (parameters.Length)
{
var parameters = methodInfo.GetParameters();
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(object))
{
found = methodInfo;
case 0:
zeroParamCandidate = GetMostDerived(zeroParamCandidate, candidate);
break;
case 1:
// Reflection can return several methods with the same parameter type when one hides another:
// only keep the most derived one, so that overridden or hidden methods are handled properly.
var parameterType = parameters[0].ParameterType;
oneParamCandidates ??= new Dictionary<Type, MethodInfo>();
oneParamCandidates[parameterType] = GetMostDerived(oneParamCandidates.GetValueOrDefault(parameterType), candidate);
break;
}
else if (parameters.Length == 0)
{
found = methodInfo;
}
}
}
_methodLookup.Add((type, methodName), found);
if (oneParamCandidates is not null)
{
// Object parameter always wins
if (oneParamCandidates.TryGetValue(typeof(object), out var objectParamCandidate))
return new MethodLookupResult(objectParamCandidate, null);
if (oneParamCandidates.Count == 1)
return new MethodLookupResult(oneParamCandidates.Values.First(), null);
var parameterTypes = oneParamCandidates.Keys
.Select(t => $"'{t.FullName}'")
.OrderBy(s => s, StringComparer.Ordinal)
.ToArray();
return new MethodLookupResult(
null,
$"Unable to resolve method of name '{methodName}' on type '{type.FullName}'. " +
$"Found {parameterTypes.Length} overloads accepting one parameter: {string.Join(", ", parameterTypes)}. " +
"Expected either a single overload with one parameter, or an overload accepting System.Object.");
}
if (zeroParamCandidate is { } found)
return new MethodLookupResult(found, null);
return new MethodLookupResult(
null,
$"Unable to resolve method of name '{methodName}' on type '{type.FullName}'. " +
$"Found {candidates.Count} overloads accepting more than one parameter. " +
"Expected a method with zero or one parameter.");
}
return found;
private static MethodInfo GetMostDerived(MethodInfo? existing, MethodInfo candidate)
{
if (existing is null)
return candidate;
return existing.DeclaringType is { } existingType &&
candidate.DeclaringType is { } candidateType &&
existingType != candidateType && existingType.IsAssignableFrom(candidateType) ?
candidate :
existing;
}
private readonly struct MethodLookupResult(MethodInfo? method, string? error)
{
public MethodInfo? Method { get; } = method;
public string? Error { get; } = error;
public bool IsMatch => Method is not null || Error is not null;
}
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]

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

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.Emit;
@ -212,23 +213,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
else if (GetAllDefinedMethods(targetType)
.Where(p => p.Name == propName.PropertyName)
.OrderByDescending(m => m.Parameters.Count)
.ToArray()
is { Length: > 0 } methodCandidates)
{
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);
}
var candidate = GetBestCommandMethod(methodCandidates, propName.PropertyName, targetType);
nodes.Add(new XamlIlClrMethodPathElementNode(candidate, context.Configuration.WellKnownTypes.Delegate, propName.AcceptsNull));
}
else
@ -453,6 +441,67 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
}
}
// Priority:
// 1. One parameter method
// 1a. Object parameter (amongst several overloads)
// 1b. Single method with one parameter
// 2. Zero parameters method
IXamlMethod GetBestCommandMethod(IXamlMethod[] candidates, string name, IXamlType targetType)
{
Debug.Assert(candidates.Length > 0);
IXamlMethod? zeroParamCandidate = null;
HashSet<IXamlMethod>? oneParamCandidates = null;
foreach (var candidate in candidates)
{
Debug.Assert(candidate.Name == name);
switch (candidate.Parameters.Count)
{
case 0:
zeroParamCandidate ??= candidate;
break;
case 1:
// Object parameter always wins
if (candidate.Parameters[0].Is("System", "Object"))
return candidate;
// Our candidates are ordered with the most derived class first:
// By adding the first candidate for a given parameter type, we automatically handle overridden or hidden methods.
oneParamCandidates ??= new HashSet<IXamlMethod>(SingleParameterTypeXamlTypeComparer.Instance);
oneParamCandidates.Add(candidate);
break;
}
}
if (oneParamCandidates is not null)
{
Debug.Assert(oneParamCandidates.Count > 0);
if (oneParamCandidates.Count == 1)
return oneParamCandidates.First();
var parameterTypes = oneParamCandidates
.Select(m => $"'{m.Parameters[0].FullName}'")
.OrderBy(s => s, StringComparer.Ordinal)
.ToArray();
throw new XamlTransformException(
$"Unable to resolve method of name '{name}' on type '{targetType}'. " +
$"Found {parameterTypes.Length} overloads accepting one parameter: {string.Join(", ", parameterTypes)}. " +
"Expected either a single overload with one parameter, or an overload accepting System.Object.",
lineInfo);
}
return zeroParamCandidate ?? throw new XamlTransformException(
$"Unable to resolve method of name '{name}' on type '{targetType}'. " +
$"Found {candidates.Length} overloads accepting more than one parameter. " +
$"Expected a method with zero or one parameter.",
lineInfo);
}
}
private static void EmitPropertyCall(XamlIlEmitContext context, IXamlILEmitter codeGen, bool acceptsNull)
@ -1064,6 +1113,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
}
}
private sealed class SingleParameterTypeXamlTypeComparer : IEqualityComparer<IXamlMethod>
{
public static SingleParameterTypeXamlTypeComparer Instance { get; } = new();
public bool Equals(IXamlMethod? x, IXamlMethod? y)
{
Debug.Assert(x is { Parameters.Count: 1 });
Debug.Assert(y is { Parameters.Count: 1 });
return x!.Parameters[0].Equals(y!.Parameters[0]);
}
public int GetHashCode(IXamlMethod obj)
{
Debug.Assert(obj.Parameters.Count == 1);
return obj.Parameters[0].GetHashCode();
}
}
}
interface IXamlIlBindingPathNode : IXamlAstValueNode

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

@ -48,9 +48,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
if (executeMethod.Parameters.Count != 0)
{
Debug.Assert(executeMethod.Parameters.Count == 1
&& executeMethod.Parameters[0] == context.Configuration.WellKnownTypes.Object);
Debug.Assert(executeMethod.Parameters.Count == 1);
gen.Ldarg(1);
if (!executeMethod.Parameters[0].Is("System", "Object"))
gen.Unbox_Any(executeMethod.Parameters[0]);
}
gen.EmitCall(executeMethod, swallowResult: true);
gen.Ret();

225
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Logging;
using Avalonia.UnitTests;
using Xunit;
@ -33,28 +35,139 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
}
}
[Theory]
[InlineData("ObjectMethod", "<x:String>hello</x:String>", "Called ObjectMethod with hello")]
[InlineData("StringMethod", "<x:String>hello</x:String>", "Called StringMethod with hello")]
[InlineData("Int32Method", "<x:Int32>42</x:Int32>", "Called Int32Method with 42")]
[InlineData("Int32Method", "<x:String>42</x:String>", "Called Int32Method with 42")]
[InlineData("VirtualObjectMethod", "<x:String>hello</x:String>", "Called VirtualObjectMethod with hello")]
[InlineData("VirtualStringMethod", "<x:String>hello</x:String>", "Called VirtualStringMethod with hello")]
[InlineData("VirtualStringMethod", "<x:Null />", "Called VirtualStringMethod with ")]
[InlineData("VirtualInt32Method", "<x:Int32>42</x:Int32>", "Called VirtualInt32Method with 42")]
[InlineData("MethodWithNewSlot", "<x:Int32>42</x:Int32>", "Called MethodWithNewSlot with 42")]
public void Binding_Method_With_Parameter_To_Command_Uses_Single_Parameter_Overload(
string methodName,
string xamlParameter,
string expected)
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
$$"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Button Name='button' Command='{Binding {{methodName}}}'>
<Button.CommandParameter>
{{xamlParameter}}
</Button.CommandParameter>
</Button>
</Window>
""");
var button = window.GetControl<Button>("button");
var vm = new ViewModel();
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal(expected, vm.Value);
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_Works()
public void Binding_Method_With_Parameter_To_Command_Prefers_Object_Overload()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Button Name='button' Command='{Binding Method1}' CommandParameter='5'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.GetControl<Button>("button");
var vm = new ViewModel();
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Button Name='button' Command='{Binding MethodWithOverloads}' CommandParameter='foo' />
</Window>
""");
var button = window.GetControl<Button>("button");
var vm = new ViewModel();
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal("Called MethodWithOverloads with Object foo", vm.Value);
}
button.DataContext = vm;
window.ApplyTemplate();
[Fact]
public void Binding_Method_With_Parameter_To_Command_Fails_With_Multiple_Single_Parameter_Overloads_Without_Object()
{
AssertBindingFails(
"MethodWithOverloads2",
"Unable to resolve method of name 'MethodWithOverloads2' on type " +
"'Avalonia.Markup.Xaml.UnitTests.Data.BindingTests_Method+ViewModel'. " +
"Found 2 overloads accepting one parameter: 'System.Int32', 'System.String'. " +
"Expected either a single overload with one parameter, or an overload accepting System.Object.");
}
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal("Called 5", vm.Value);
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_Uses_Parameterless_Overload_When_No_Overloads_With_Parameter_Exist()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Button Name='button' Command='{Binding MethodWithOverloads3}' CommandParameter='foo' />
</Window>
""");
var button = window.GetControl<Button>("button");
var vm = new ViewModel();
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal("Called MethodWithOverloads3 without parameter", vm.Value);
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_Fails_Without_Valid_Overloads()
{
AssertBindingFails(
"MethodWithOverloads4",
"Unable to resolve method of name 'MethodWithOverloads4' on type " +
"'Avalonia.Markup.Xaml.UnitTests.Data.BindingTests_Method+ViewModel'. " +
"Found 2 overloads accepting more than one parameter. " +
"Expected a method with zero or one parameter.");
}
private static void AssertBindingFails(string methodName, string expectedError)
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var errors = new List<string>();
using var logSink = TestLogSink.Start((level, area, _, template, values) =>
{
if (level >= LogEventLevel.Warning && area == LogArea.Binding)
errors.Add(template + " " + string.Join(" ", values));
});
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
$$"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Button Name='button' Command='{Binding {{methodName}}}' CommandParameter='foo' />
</Window>
""");
var button = window.GetControl<Button>("button");
var vm = new ViewModel();
button.DataContext = vm;
window.ApplyTemplate();
Assert.Null(button.Command);
Assert.Contains(errors, error => error.Contains(expectedError, StringComparison.Ordinal));
}
[Fact]
@ -141,28 +254,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
}
}
[Fact]
public void Binding_Method_Preserves_Correct_Order()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Button Name='button' Command='{Binding Method3}' CommandParameter='5'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.GetControl<Button>("button");
var vm = new ViewModel();
button.DataContext = vm;
window.ApplyTemplate();
PerformClick(button);
Assert.Equal("Called Method with parameter of object type. Argument value is 5", vm.Value);
}
}
[Fact]
public void Binding_Method_To_Command_Collected()
{
@ -217,15 +308,57 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
});
}
private class ViewModel : INotifyPropertyChanged
private class ViewModelBase
{
public virtual void VirtualObjectMethod(object? i) { }
public virtual void VirtualInt32Method(int i) { }
public virtual void VirtualStringMethod(string i) { }
public void MethodWithNewSlot(int i) { }
}
private class ViewModel : ViewModelBase, INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string Method() => Value = "Called";
public string Method1(object i) => Value = $"Called {i}";
public string Method2(int i, int j) => Value = $"Called {i},{j}";
public string Method3() => Value = "Called";
public string Method3(object obj) => Value = $"Called Method with parameter of object type. Argument value is {obj}";
public void Method() => Value = "Called";
public void ObjectMethod(object i) => Value = $"Called ObjectMethod with {i}";
public void Int32Method(int i) => Value = $"Called Int32Method with {i}";
public void StringMethod(string i) => Value = $"Called StringMethod with {i}";
public void MethodWithOverloads() => Value = "Called MethodWithOverloads without parameter";
public void MethodWithOverloads(int i) => Value = $"Called MethodWithOverloads with Int32 {i}";
public void MethodWithOverloads(string i) => Value = $"Called MethodWithOverloads with String {i}";
public void MethodWithOverloads(object i) => Value = $"Called MethodWithOverloads with Object {i}";
public void MethodWithOverloads2() => Value = "Called MethodWithOverloads2 without parameter";
public void MethodWithOverloads2(int i) => Value = $"Called MethodWithOverloads2 with Int32 {i}";
public void MethodWithOverloads2(string i) => Value = $"Called MethodWithOverloads2 with String {i}";
public void MethodWithOverloads3() => Value = "Called MethodWithOverloads3 without parameter";
public void MethodWithOverloads3(int a, int b) => throw new InvalidOperationException("MethodWithOverloads3 should not be called");
public void MethodWithOverloads3(string a, string b) => throw new InvalidOperationException("MethodWithOverloads3 should not be called");
public void MethodWithOverloads4(int a, int b) => throw new InvalidOperationException("MethodWithOverloads4 should not be called");
public void MethodWithOverloads4(string a, string b) => throw new InvalidOperationException("MethodWithOverloads4 should not be called");
public override void VirtualObjectMethod(object? i)
=> Value = $"Called VirtualObjectMethod with {i}";
public override void VirtualInt32Method(int i)
=> Value = $"Called VirtualInt32Method with {i}";
public override void VirtualStringMethod(string i)
=> Value = $"Called VirtualStringMethod with {i}";
public new void MethodWithNewSlot(int i)
=> Value = $"Called MethodWithNewSlot with {i}";
public string Value { get; private set; } = "Not called";
private object? _parameter;

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

@ -1905,31 +1905,172 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
[Theory]
[InlineData("5")]
[InlineData("hello")]
[InlineData(null)]
public void Binding_Method_With_Parameter_To_Command_Works(string? parameter)
[InlineData("ObjectMethod", "<x:String>hello</x:String>", "Called ObjectMethod with hello")]
[InlineData("StringMethod", "<x:String>hello</x:String>", "Called StringMethod with hello")]
[InlineData("StringMethod", "<x:Null />", "Called StringMethod with ")]
[InlineData("Int32Method", "<x:Int32>42</x:Int32>", "Called Int32Method with 42")]
[InlineData("VirtualObjectMethod", "<x:String>hello</x:String>", "Called VirtualObjectMethod with hello")]
[InlineData("VirtualStringMethod", "<x:String>hello</x:String>", "Called VirtualStringMethod with hello")]
[InlineData("VirtualStringMethod", "<x:Null />", "Called VirtualStringMethod with ")]
[InlineData("VirtualInt32Method", "<x:Int32>42</x:Int32>", "Called VirtualInt32Method with 42")]
[InlineData("MethodWithNewSlot", "<x:Int32>42</x:Int32>", "Called MethodWithNewSlot with 42")]
public void Binding_Method_With_Parameter_To_Command_Uses_Single_Parameter_Overload(
string methodName,
string xamlParameter,
string expected)
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
$$"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:MethodAsCommandDataContext'>
<Button Name='button' Command='{CompiledBinding {{methodName}}}'>
<Button.CommandParameter>
{{xamlParameter}}
</Button.CommandParameter>
</Button>
</Window>
""");
var button = window.GetControl<Button>("button");
var vm = new MethodAsCommandDataContext();
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal(expected, vm.Value);
}
[Theory]
[InlineData("Int32Method", "<x:String>hello</x:String>", typeof(InvalidCastException))]
[InlineData("Int32Method", "<x:Null />", typeof(NullReferenceException))]
[InlineData("StringMethod", "<x:Int32>42</x:Int32>", typeof(InvalidCastException))]
public void Binding_Method_With_Parameter_To_Command_With_Single_Parameter_Overload_Throws_At_Runtime_If_Mismatched_Types(
string methodName,
string xamlParameter,
Type exceptionType)
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = $@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:MethodAsCommandDataContext'>
<Button Name='button' Command='{{CompiledBinding Method1}}' CommandParameter='{ parameter ?? "{x:Null}" }'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.GetControl<Button>("button");
var vm = new MethodAsCommandDataContext();
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
button.DataContext = vm;
window.ApplyTemplate();
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
$$"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:MethodAsCommandDataContext'>
<Button Name='button' Command='{CompiledBinding {{methodName}}}'>
<Button.CommandParameter>
{{xamlParameter}}
</Button.CommandParameter>
</Button>
</Window>
""");
var button = window.GetControl<Button>("button");
var vm = new MethodAsCommandDataContext();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal("Called " + parameter, vm.Value);
}
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
Assert.Throws(exceptionType, () => PerformClick(button));
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_Prefers_Object_Overload()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:MethodAsCommandDataContext'>
<Button Name='button' Command='{CompiledBinding MethodWithOverloads}' CommandParameter="foo" />
</Window>
""");
var button = window.GetControl<Button>("button");
var vm = new MethodAsCommandDataContext();
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal("Called MethodWithOverloads with Object foo", vm.Value);
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_Fails_With_Multiple_Single_Parameter_Overloads_Without_Object()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var exception = Assert.ThrowsAny<XmlException>(() => (Window)AvaloniaRuntimeXamlLoader.Load(
"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:MethodAsCommandDataContext'>
<Button Name='button' Command='{CompiledBinding MethodWithOverloads2}' CommandParameter="foo" />
</Window>
"""));
Assert.StartsWith(
"Unable to resolve method of name 'MethodWithOverloads2' on type 'Avalonia.Markup.Xaml.UnitTests.MarkupExtensions.MethodAsCommandDataContext'. " +
"Found 2 overloads accepting one parameter: 'System.Int32', 'System.String'. " +
"Expected either a single overload with one parameter, or an overload accepting System.Object.",
exception.Message);
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_Uses_Parameterless_Overload_When_No_Overloads_With_Parameter_Exist()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var window = (Window)AvaloniaRuntimeXamlLoader.Load(
"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:MethodAsCommandDataContext'>
<Button Name='button' Command='{CompiledBinding MethodWithOverloads3}' CommandParameter="foo" />
</Window>
""");
var button = window.GetControl<Button>("button");
var vm = new MethodAsCommandDataContext();
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal("Called MethodWithOverloads3 without parameter", vm.Value);
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_Fails_Without_Valid_Overloads()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var exception = Assert.ThrowsAny<XmlException>(() => (Window)AvaloniaRuntimeXamlLoader.Load(
"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:MethodAsCommandDataContext'>
<Button Name='button' Command='{CompiledBinding MethodWithOverloads4}' CommandParameter="foo" />
</Window>
"""));
Assert.StartsWith(
"Unable to resolve method of name 'MethodWithOverloads4' on type 'Avalonia.Markup.Xaml.UnitTests.MarkupExtensions.MethodAsCommandDataContext'. " +
"Found 2 overloads accepting more than one parameter. " +
"Expected a method with zero or one parameter. ",
exception.Message);
}
[Fact]
@ -2541,15 +2682,57 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public object CustomDelegateTypeInt(object i) => i;
}
public class MethodAsCommandDataContext : INotifyPropertyChanged
public class MethodAsCommandDataContextBase
{
public virtual void VirtualObjectMethod(object? i) { }
public virtual void VirtualInt32Method(int i) { }
public virtual void VirtualStringMethod(string i) { }
public void MethodWithNewSlot(int i) { }
}
public class MethodAsCommandDataContext : MethodAsCommandDataContextBase, INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string Method() => Value = "Called";
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 void Method() => Value = "Called";
public void ObjectMethod(object i) => Value = $"Called ObjectMethod with {i}";
public void Int32Method(int i) => Value = $"Called Int32Method with {i}";
public void StringMethod(string i) => Value = $"Called StringMethod with {i}";
public void MethodWithOverloads() => Value = "Called MethodWithOverloads without parameter";
public void MethodWithOverloads(int i) => Value = $"Called MethodWithOverloads with Int32 {i}";
public void MethodWithOverloads(string i) => Value = $"Called MethodWithOverloads with String {i}";
public void MethodWithOverloads(object i) => Value = $"Called MethodWithOverloads with Object {i}";
public void MethodWithOverloads2() => Value = "Called MethodWithOverloads2 without parameter";
public void MethodWithOverloads2(int i) => Value = $"Called MethodWithOverloads2 with Int32 {i}";
public void MethodWithOverloads2(string i) => Value = $"Called MethodWithOverloads2 with String {i}";
public void MethodWithOverloads3() => Value = "Called MethodWithOverloads3 without parameter";
public void MethodWithOverloads3(int a, int b) => throw new InvalidOperationException("MethodWithOverloads3 should not be called");
public void MethodWithOverloads3(string a, string b) => throw new InvalidOperationException("MethodWithOverloads3 should not be called");
public void MethodWithOverloads4(int a, int b) => throw new InvalidOperationException("MethodWithOverloads4 should not be called");
public void MethodWithOverloads4(string a, string b) => throw new InvalidOperationException("MethodWithOverloads4 should not be called");
public override void VirtualObjectMethod(object? i)
=> Value = $"Called VirtualObjectMethod with {i}";
public override void VirtualInt32Method(int i)
=> Value = $"Called VirtualInt32Method with {i}";
public override void VirtualStringMethod(string i)
=> Value = $"Called VirtualStringMethod with {i}";
public new void MethodWithNewSlot(int i)
=> Value = $"Called MethodWithNewSlot with {i}";
public string Value { get; private set; } = "Not called";
private object? _parameter;

Loading…
Cancel
Save