Browse Source

Merge pull request #1179 from jkoritzinsky/MethodsAsCommands

Bind Commands to Methods
pull/1204/head
Jeremy Koritzinsky 9 years ago
committed by GitHub
parent
commit
38091ee670
  1. 43
      src/Markup/Avalonia.Markup/AlwaysEnabledDelegateCommand.cs
  2. 1
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  3. 87
      src/Markup/Avalonia.Markup/Data/Plugins/MethodAccessorPlugin.cs
  4. 6
      src/Markup/Avalonia.Markup/DefaultValueConverter.cs
  5. 22
      tests/Avalonia.LeakTests/ExpressionObserverTests.cs
  6. 87
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Method.cs
  7. 38
      tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs
  8. 105
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs

43
src/Markup/Avalonia.Markup/AlwaysEnabledDelegateCommand.cs

@ -0,0 +1,43 @@
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Windows.Input;
namespace Avalonia.Markup
{
class AlwaysEnabledDelegateCommand : ICommand
{
private readonly Delegate action;
private ParameterInfo parameterInfo;
public AlwaysEnabledDelegateCommand(Delegate action)
{
this.action = action;
var parameters = action.Method.GetParameters();
parameterInfo = parameters.Length == 0 ? null : parameters[0];
}
#pragma warning disable 0067
public event EventHandler CanExecuteChanged;
#pragma warning restore 0067
public bool CanExecute(object parameter) => true;
public void Execute(object parameter)
{
if (parameterInfo == null)
{
action.DynamicInvoke();
}
else
{
TypeUtilities.TryConvert(parameterInfo.ParameterType, parameter, CultureInfo.CurrentCulture, out object convertedParameter);
action.DynamicInvoke(convertedParameter);
}
}
}
}

1
src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs

@ -25,6 +25,7 @@ namespace Avalonia.Markup.Data
new List<IPropertyAccessorPlugin>
{
new AvaloniaPropertyAccessorPlugin(),
new MethodAccessorPlugin(),
new InpcPropertyAccessorPlugin(),
};

87
src/Markup/Avalonia.Markup/Data/Plugins/MethodAccessorPlugin.cs

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data;
using System.Reflection;
using System.Linq;
namespace Avalonia.Markup.Data.Plugins
{
class MethodAccessorPlugin : IPropertyAccessorPlugin
{
public bool Match(object obj, string methodName)
=> obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName);
public IPropertyAccessor Start(WeakReference reference, string methodName)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(methodName != null);
var instance = reference.Target;
var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName);
if (method != null)
{
if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8)
{
var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(method));
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
return new Accessor(reference, method);
}
else
{
var message = $"Could not find CLR method '{methodName}' on '{instance}'";
var exception = new MissingMemberException(message);
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
}
private class Accessor : PropertyAccessorBase
{
public Accessor(WeakReference reference, MethodInfo method)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(method != null);
var paramTypes = method.GetParameters().Select(param => param.ParameterType).ToArray();
var returnType = method.ReturnType;
if (returnType == typeof(void))
{
if (paramTypes.Length == 0)
{
PropertyType = typeof(Action);
}
else
{
PropertyType = Type.GetType($"System.Action`{paramTypes.Length}").MakeGenericType(paramTypes);
}
}
else
{
var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray();
PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters);
}
Value = method.IsStatic ? method.CreateDelegate(PropertyType) : method.CreateDelegate(PropertyType, reference.Target);
}
public override Type PropertyType { get; }
public override object Value { get; }
public override bool SetValue(object value, BindingPriority priority) => false;
protected override void SubscribeCore(IObserver<object> observer)
{
try
{
Observer.OnNext(Value);
}
catch { }
}
}
}
}

6
src/Markup/Avalonia.Markup/DefaultValueConverter.cs

@ -5,6 +5,7 @@ using System;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Utilities;
using System.Windows.Input;
namespace Avalonia.Markup
{
@ -34,6 +35,11 @@ namespace Avalonia.Markup
return AvaloniaProperty.UnsetValue;
}
if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)
{
return new AlwaysEnabledDelegateCommand(d);
}
if (TypeUtilities.TryConvert(targetType, value, culture, out object result))
{
return result;

22
tests/Avalonia.LeakTests/ExpressionObserverTests.cs

@ -71,6 +71,28 @@ namespace Avalonia.LeakTests
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<NonIntegerIndexer>()).ObjectsCount));
}
[Fact]
public void Should_Not_Keep_Source_Alive_MethodBinding()
{
Func<ExpressionObserver> run = () =>
{
var source = new { Foo = new MethodBound() };
var target = new ExpressionObserver(source, "Foo.A");
target.Subscribe(_ => { });
return target;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<MethodBound>()).ObjectsCount));
}
private class MethodBound
{
public void A() { }
}
private class NonIntegerIndexer : NotifyingBase
{
private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();

87
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Method.cs

@ -0,0 +1,87 @@
using Avalonia.Data;
using Avalonia.Markup.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class ExpressionObserverTests_Method
{
private class TestObject
{
public void MethodWithoutReturn() { }
public int MethodWithReturn() => 0;
public int MethodWithReturnAndParameters(int i) => i;
public static void StaticMethod() { }
public static void TooManyParameters(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9) { }
public static int TooManyParametersWithReturnType(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) => 1;
}
[Fact]
public async Task Should_Get_Method()
{
var data = new TestObject();
var observer = new ExpressionObserver(data, nameof(TestObject.MethodWithoutReturn));
var result = await observer.Take(1);
Assert.NotNull(result);
GC.KeepAlive(data);
}
[Theory]
[InlineData(nameof(TestObject.MethodWithoutReturn), typeof(Action))]
[InlineData(nameof(TestObject.MethodWithReturn), typeof(Func<int>))]
[InlineData(nameof(TestObject.MethodWithReturnAndParameters), typeof(Func<int, int>))]
[InlineData(nameof(TestObject.StaticMethod), typeof(Action))]
public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, Type expectedType)
{
var data = new TestObject();
var observer = new ExpressionObserver(data, methodName);
var result = await observer.Take(1);
Assert.IsType(expectedType, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Can_Call_Method_Returned_From_Observer()
{
var data = new TestObject();
var observer = new ExpressionObserver(data, nameof(TestObject.MethodWithReturnAndParameters));
var result = await observer.Take(1);
var callback = (Func<int, int>)result;
Assert.Equal(1, callback(1));
GC.KeepAlive(data);
}
[Theory]
[InlineData(nameof(TestObject.TooManyParameters))]
[InlineData(nameof(TestObject.TooManyParametersWithReturnType))]
public async Task Should_Return_Error_Notification_If_Too_Many_Parameters(string methodName)
{
var data = new TestObject();
var observer = new ExpressionObserver(data, methodName);
var result = await observer.Take(1);
Assert.IsType<BindingNotification>(result);
Assert.Equal(BindingErrorType.Error, ((BindingNotification)result).ErrorType);
GC.KeepAlive(data);
}
}
}

38
tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs

@ -5,6 +5,8 @@ using System.Globalization;
using Avalonia.Controls;
using Avalonia.Data;
using Xunit;
using System.Windows.Input;
using System;
namespace Avalonia.Markup.UnitTests
{
@ -118,6 +120,42 @@ namespace Avalonia.Markup.UnitTests
Assert.IsType<BindingNotification>(result);
}
[Fact]
public void Can_Convert_From_Delegate_To_Command()
{
int commandResult = 0;
var result = DefaultValueConverter.Instance.Convert(
(Action<int>)((int i) => { commandResult = i; }),
typeof(ICommand),
null,
CultureInfo.InvariantCulture);
Assert.IsAssignableFrom<ICommand>(result);
(result as ICommand).Execute(5);
Assert.Equal(5, commandResult);
}
[Fact]
public void Can_Convert_From_Delegate_To_Command_No_Parameters()
{
int commandResult = 0;
var result = DefaultValueConverter.Instance.Convert(
(Action)(() => { commandResult = 1; }),
typeof(ICommand),
null,
CultureInfo.InvariantCulture);
Assert.IsAssignableFrom<ICommand>(result);
(result as ICommand).Execute(null);
Assert.Equal(1, commandResult);
}
private enum TestEnum
{
Foo,

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

@ -0,0 +1,105 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Reactive.Subjects;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
public class BindingTests_Method
{
[Fact]
public void Binding_Method_To_Command_Works()
{
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 Method}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
var vm = new ViewModel();
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal("Called", vm.Value);
}
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_Works()
{
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 loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
var vm = new ViewModel();
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal("Called 5", vm.Value);
}
}
[Fact]
public void Binding_Method_To_TextBlock_Text_Works()
{
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'>
<TextBlock Name='textBlock' Text='{Binding Method}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
var vm = new ViewModel();
textBlock.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(textBlock.Text);
}
}
static void PerformClick(Button button)
{
button.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Input.Key.Enter,
});
}
private class ViewModel
{
public string Method() => Value = "Called";
public string Method1(int i) => Value = $"Called {i}";
public string Method2(int i, int j) => Value = $"Called {i},{j}";
public string Value { get; private set; } = "Not called";
}
}
}
Loading…
Cancel
Save