Browse Source

Added support for binding to Observables and Tasks

Thread synchronization is not currently implemented. Task tests fail
because of this: putting a Thread.Sleep before the Assert makes them
pass. Need to work out how we're going to do async unit testing.
Steven Kirk 10 years ago
parent
commit
9b71fc9ce9

+ 41 - 4
src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs

@@ -3,13 +3,18 @@
 
 using System;
 using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
 using System.Reflection;
+using System.Threading.Tasks;
+using Perspex.Threading;
 
 namespace Perspex.Markup.Binding
 {
     internal class PropertyAccessorNode : ExpressionNode
     {
         private PropertyInfo _propertyInfo;
+        private IDisposable _subscription;
 
         public PropertyAccessorNode(string propertyName)
         {
@@ -38,7 +43,7 @@ namespace Perspex.Markup.Binding
 
         protected override void SubscribeAndUpdate(object target)
         {
-            var result = ExpressionValue.None;
+            bool set = false;
 
             if (target != null)
             {
@@ -46,7 +51,8 @@ namespace Perspex.Markup.Binding
 
                 if (_propertyInfo != null)
                 {
-                    result = new ExpressionValue(_propertyInfo.GetValue(target));
+                    ReadValue(target);
+                    set = true;
 
                     var inpc = target as INotifyPropertyChanged;
 
@@ -61,7 +67,10 @@ namespace Perspex.Markup.Binding
                 _propertyInfo = null;
             }
 
-            CurrentValue = result;
+            if (!set)
+            {
+                CurrentValue = ExpressionValue.None;
+            }
         }
 
         protected override void Unsubscribe(object target)
@@ -74,11 +83,39 @@ namespace Perspex.Markup.Binding
             }
         }
 
+        private void ReadValue(object target)
+        {
+            var value = _propertyInfo.GetValue(target);
+            var observable = value as IObservable<object>;
+            var task = value as Task;
+
+            if (observable != null)
+            {
+                CurrentValue = ExpressionValue.None;
+                _subscription = observable
+                    .Subscribe(x => CurrentValue = new ExpressionValue(x));
+            }
+            else if (task != null)
+            {
+                var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result");
+
+                if (resultProperty != null)
+                {
+                    task.ContinueWith(x => CurrentValue = new ExpressionValue(resultProperty.GetValue(task)))                        
+                        .ConfigureAwait(false);
+                }
+            }
+            else
+            {
+                CurrentValue = new ExpressionValue(value);
+            }
+        }
+
         private void PropertyChanged(object sender, PropertyChangedEventArgs e)
         {
             if (e.PropertyName == PropertyName)
             {
-                CurrentValue = new ExpressionValue(_propertyInfo.GetValue(Target));
+                ReadValue(sender);
             }
         }
     }

+ 61 - 0
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs

@@ -0,0 +1,61 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using Perspex.Markup.Binding;
+using Xunit;
+
+namespace Perspex.Markup.UnitTests.Binding
+{
+    public class ExpressionObserverTests_Observable
+    {
+        [Fact]
+        public void Should_Get_Simple_Observable_Value()
+        {
+            var source = new BehaviorSubject<string>("foo");
+            var data = new { Foo = source };
+            var target = new ExpressionObserver(data, "Foo");
+            var result = new List<object>();
+
+            var sub = target.Subscribe(x => result.Add(x.Value));
+            source.OnNext("bar");
+
+            Assert.Equal(new[] { "foo", "bar" }, result);
+        }
+
+        [Fact]
+        public void Should_Get_Property_Value_From_Observable()
+        {
+            var data = new Class1();
+            var target = new ExpressionObserver(data, "Next.Foo");
+            var result = new List<object>();
+
+            var sub = target.Subscribe(x => result.Add(x.Value));
+            data.Next.OnNext(new Class2("foo"));
+
+            Assert.Equal(new[] { null, "foo" }, result);
+
+            sub.Dispose();
+
+            Assert.Equal(0, data.SubscriptionCount);
+        }
+
+        private class Class1 : NotifyingBase
+        {
+            public Subject<Class2> Next { get; } = new Subject<Class2>();
+        }
+
+        private class Class2 : NotifyingBase
+        {
+            public Class2(string foo)
+            {
+                Foo = foo;
+            }
+
+            public string Foo { get; }
+        }
+    }
+}

+ 63 - 0
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs

@@ -0,0 +1,63 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Perspex.Markup.Binding;
+using Xunit;
+
+namespace Perspex.Markup.UnitTests.Binding
+{
+    public class ExpressionObserverTests_Task
+    {
+        [Fact]
+        public void Should_Get_Simple_Task_Value()
+        {
+            var tcs = new TaskCompletionSource<string>();
+            var data = new { Foo = tcs.Task };
+            var target = new ExpressionObserver(data, "Foo");
+            var result = new List<object>();
+
+            var sub = target.Subscribe(x => result.Add(x.Value));
+            tcs.SetResult("foo");
+
+            Assert.Equal(new object[] { null, "foo" }, result.ToArray());
+        }
+        
+        [Fact]
+        public void Should_Get_Property_Value_From_Task()
+        {
+            var tcs = new TaskCompletionSource<Class2>();
+            var data = new Class1(tcs.Task);
+            var target = new ExpressionObserver(data, "Next.Foo");
+            var result = new List<object>();
+
+            var sub = target.Subscribe(x => result.Add(x.Value));
+            tcs.SetResult(new Class2("foo"));
+
+            Assert.Equal(new object[] { null, "foo" }, result.ToArray());
+        }
+
+        private class Class1 : NotifyingBase
+        {
+            public Class1(Task<Class2> next)
+            {
+                Next = next;
+            }
+
+            public Task<Class2> Next { get; }
+        }
+
+        private class Class2 : NotifyingBase
+        {
+            public Class2(string foo)
+            {
+                Foo = foo;
+            }
+
+            public string Foo { get; }
+        }
+    }
+}

+ 3 - 0
tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

@@ -72,6 +72,9 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Binding\ExpressionNodeBuilderTests_Errors.cs" />
+    <Compile Include="Binding\ExpressionObserverTests_Observable.cs" />
+    <Compile Include="Binding\ExpressionObserverTests_Task.cs" />
     <Compile Include="Binding\ExpressionObserverTests_Indexer.cs" />
     <Compile Include="Binding\ExpressionObserverTests_Negation.cs" />
     <Compile Include="Binding\ExpressionObserverTests_Property.cs" />