From 12ec059acb3e83b718eabda9cf7f7ac6b8ed5043 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 2 Oct 2017 18:08:37 -0500 Subject: [PATCH] Initial code to enable binding to a method. --- .../Data/ExpressionObserver.cs | 1 + .../Data/Plugins/MethodAccessorPlugin.cs | 91 +++++++++++++++++++ .../Data/ExpressionObserverTests_Method.cs | 65 +++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 src/Markup/Avalonia.Markup/Data/Plugins/MethodAccessorPlugin.cs create mode 100644 tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Method.cs diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index 1e55e17195..66d3beb907 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -25,6 +25,7 @@ namespace Avalonia.Markup.Data new List { new AvaloniaPropertyAccessorPlugin(), + new MethodAccessorPlugin(), new InpcPropertyAccessorPlugin(), }; diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/MethodAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/MethodAccessorPlugin.cs new file mode 100644 index 0000000000..ef4281c314 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/MethodAccessorPlugin.cs @@ -0,0 +1,91 @@ +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 propertyName) + => obj.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == propertyName) != null; + + public IPropertyAccessor Start(WeakReference reference, string propertyName) + { + Contract.Requires(reference != null); + Contract.Requires(propertyName != null); + + var instance = reference.Target; + var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == propertyName); + + if (method != null) + { + return new Accessor(reference, method); + } + else + { + var message = $"Could not find CLR method '{propertyName}' 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(reference != null); + Contract.Requires(method != null); + + var paramTypes = method.GetParameters().Select(param => param.ParameterType).ToArray(); + var returnType = method.ReturnType; + + // TODO: Throw exception if more than 8 parameters or more than 7 + return type. + // Do this here or in the caller? Here probably + 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); + } + + // TODO: Is this going to leak? + // TODO: Static methods? + Value = 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 observer) + { + SendCurrentValue(); + } + + private void SendCurrentValue() + { + try + { + var value = Value; + Observer.OnNext(value); + } + catch { } + } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Method.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Method.cs new file mode 100644 index 0000000000..9929e4e986 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Method.cs @@ -0,0 +1,65 @@ +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; + } + + [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))] + [InlineData(nameof(TestObject.MethodWithReturnAndParameters), typeof(Func))] + 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)result; + + Assert.Equal(1, callback(1)); + + GC.KeepAlive(data); + } + } +}