From f3029d33463494ed60c8d27aa4ebb1cb918534f2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 19 Jan 2019 18:04:30 +0100 Subject: [PATCH 1/2] Added failing test for #2203. --- .../ExpressionObserverTests_Observable.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs index 701fdbce9c..4585181ab7 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs @@ -150,6 +150,26 @@ namespace Avalonia.Base.UnitTests.Data.Core } } + [Fact] + public void Should_Work_With_Value_Type() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var source = new BehaviorSubject(1); + var data = new { Foo = source }; + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); + var result = new List(); + + var sub = target.Subscribe(x => result.Add((int)x)); + source.OnNext(42); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new[] { 1, 42 }, result); + + GC.KeepAlive(data); + } + } + private class Class1 : NotifyingBase { public Subject Next { get; } = new Subject(); From d3e8752f15eb90db33707aa592e681ba37fda1a7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 19 Jan 2019 18:05:13 +0100 Subject: [PATCH 2/2] Make ObservableStreamPlugin work with value types. Use reflection to call `Observable.Select` on the source observable to box the value. --- .../Core/Plugins/ObservableStreamPlugin.cs | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index 14ca8ee79e..c41097c274 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; +using System.Reactive.Linq; +using System.Reflection; namespace Avalonia.Data.Core.Plugins { @@ -10,12 +13,19 @@ namespace Avalonia.Data.Core.Plugins /// public class ObservableStreamPlugin : IStreamPlugin { + static MethodInfo observableSelect; + /// /// Checks whether this plugin handles the specified value. /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - public virtual bool Match(WeakReference reference) => reference.Target is IObservable; + public virtual bool Match(WeakReference reference) + { + return reference.Target.GetType().GetInterfaces().Any(x => + x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(IObservable<>)); + } /// /// Starts producing output based on the specified value. @@ -26,7 +36,69 @@ namespace Avalonia.Data.Core.Plugins /// public virtual IObservable Start(WeakReference reference) { - return reference.Target as IObservable; + var target = reference.Target; + + // If the observable returns a reference type then we can cast it. + if (target is IObservable result) + { + return result; + }; + + // If the observable returns a value type then we need to call Observable.Select on it. + // First get the type of T in `IObservable`. + var sourceType = reference.Target.GetType().GetInterfaces().First(x => + x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0]; + + // Get the Observable.Select method. + var select = GetObservableSelect(sourceType); + + // Make a Box<> delegate of the correct type. + var funcType = typeof(Func<,>).MakeGenericType(sourceType, typeof(object)); + var box = GetType().GetMethod(nameof(Box), BindingFlags.Static | BindingFlags.NonPublic) + .MakeGenericMethod(sourceType) + .CreateDelegate(funcType); + + // Call Observable.Select(target, box); + return (IObservable)select.Invoke( + null, + new object[] { target, box }); + } + + private static MethodInfo GetObservableSelect(Type source) + { + return GetObservableSelect().MakeGenericMethod(source, typeof(object)); } + + private static MethodInfo GetObservableSelect() + { + if (observableSelect == null) + { + observableSelect = typeof(Observable).GetRuntimeMethods().First(x => + { + if (x.Name == nameof(Observable.Select) && + x.ContainsGenericParameters && + x.GetGenericArguments().Length == 2) + { + var parameters = x.GetParameters(); + + if (parameters.Length == 2 && + parameters[0].ParameterType.IsConstructedGenericType && + parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IObservable<>) && + parameters[1].ParameterType.IsConstructedGenericType && + parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>)) + { + return true; + } + } + + return false; + }); + } + + return observableSelect; + } + + private static object Box(T value) => (object)value; } }