Browse Source

Make tests in Avalonia.Base.UnitTests use ExpressionObserver.Create. For tests that require using invalid members or are more tedious to test with expression trees, test them in Avalonia.Markup.UnitTests with ExpressionObserverBuilder.

Jeremy Koritzinsky 7 years ago
parent
commit
1b2d644e48
23 changed files with 959 additions and 379 deletions
  1. 12 2
      src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs
  2. 15 12
      src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs
  3. 5 0
      src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs
  4. 1 1
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  5. 20 20
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  6. 5 42
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs
  7. 6 4
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs
  8. 12 19
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  9. 16 0
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs
  10. 20 68
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs
  11. 8 8
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs
  12. 2 94
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs
  13. 14 9
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs
  14. 40 79
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  15. 16 12
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs
  16. 7 7
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs
  17. 9 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs
  18. 165 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs
  19. 59 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs
  20. 371 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs
  21. 2 2
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs
  22. 112 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs
  23. 42 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs

+ 12 - 2
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Linq.Expressions;
+using System.Reflection;
 using System.Text;
 using Avalonia.Data;
 
@@ -48,12 +49,21 @@ namespace Avalonia.Data.Core
 
         protected override object GetValue(object target)
         {
-            return getDelegate.DynamicInvoke(target);
+            try
+            {
+                return getDelegate.DynamicInvoke(target);
+            }
+            catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException
+                                                        || e.InnerException is IndexOutOfRangeException
+                                                        || e.InnerException is KeyNotFoundException)
+            {
+                return AvaloniaProperty.UnsetValue;
+            }
         }
 
         protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
         {
-            return expression.Indexer.Name == e.PropertyName;
+            return expression.Indexer == null || expression.Indexer.Name == e.PropertyName;
         }
 
         protected override int? TryGetFirstArgumentAsInt() => firstArgumentDelegate.DynamicInvoke(Target.Target) as int?;

+ 15 - 12
src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs

@@ -10,6 +10,7 @@ namespace Avalonia.Data.Core.Parsers
 {
     class ExpressionVisitorNodeBuilder : ExpressionVisitor
     {
+        private const string MultiDimensionalArrayGetterMethodName = "Get";
         private static PropertyInfo AvaloniaObjectIndexer;
         private static MethodInfo CreateDelegateMethod;
 
@@ -98,7 +99,6 @@ namespace Avalonia.Data.Core.Parsers
         {
             if (node.NodeType == ExpressionType.ArrayIndex)
             {
-                base.VisitBinary(node);
                 return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right }));
             }
             throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
@@ -161,21 +161,13 @@ namespace Avalonia.Data.Core.Parsers
 
         protected override Expression VisitMethodCall(MethodCallExpression node)
         {
-            var property = TryGetPropertyFromMethod(node.Method);
-
-            if (property != null)
-            {
-                return Visit(Expression.MakeIndex(node.Object, property, node.Arguments));
-            }
-
             if (node.Method == CreateDelegateMethod)
             {
                 var visited = Visit(node.Arguments[1]);
                 Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue<MethodInfo>(node.Object).Name, enableDataValidation));
-                return visited;
+                return node;
             }
-
-            if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`'))
+            else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`'))
             {
                 if (node.Method.IsStatic)
                 {
@@ -189,7 +181,18 @@ namespace Avalonia.Data.Core.Parsers
                 return node;
             }
 
-            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+            var property = TryGetPropertyFromMethod(node.Method);
+
+            if (property != null)
+            {
+                return Visit(Expression.MakeIndex(node.Object, property, node.Arguments));
+            }
+            else if (node.Object.Type.IsArray && node.Method.Name == MultiDimensionalArrayGetterMethodName)
+            {
+                return Visit(Expression.MakeIndex(node.Object, null, node.Arguments));
+            }
+
+            throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType.AssemblyQualifiedName}.{node.Method.Name}'.");
         }
 
         private PropertyInfo TryGetPropertyFromMethod(MethodInfo method)

+ 5 - 0
src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs

@@ -14,6 +14,11 @@ namespace Avalonia
             throw new InvalidOperationException("This should be used only in a binding expression");
         }
 
+        public static object StreamBinding(this Task @this)
+        {
+            throw new InvalidOperationException("This should be used only in a binding expression");
+        }
+
         public static T StreamBinding<T>(this IObservable<T> @this)
         {
             throw new InvalidOperationException("This should be used only in a binding expression");

+ 1 - 1
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs

@@ -6,7 +6,7 @@ using System.Text;
 
 namespace Avalonia.Markup.Parsers
 {
-    public class ExpressionObserverBuilder
+    public static class ExpressionObserverBuilder
     {
         internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
         {

+ 20 - 20
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_Property_Value()
         {
             var data = new Class1 { StringValue = "foo" };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
             var result = await target.Take(1);
 
             Assert.Equal("foo", result);
@@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Set_Simple_Property_Value()
         {
             var data = new Class1 { StringValue = "foo" };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
 
             target.OnNext("bar");
 
@@ -48,7 +48,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Set_Indexed_Value()
         {
             var data = new { Foo = new[] { "foo" } };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "Foo[0]"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.Foo[0]), typeof(string));
 
             target.OnNext("bar");
 
@@ -61,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Convert_Get_String_To_Double()
         {
             var data = new Class1 { StringValue = $"{5.6}" };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
             var result = await target.Take(1);
 
             Assert.Equal(5.6, result);
@@ -73,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Getting_Invalid_Double_String_Should_Return_BindingError()
         {
             var data = new Class1 { StringValue = "foo" };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
             var result = await target.Take(1);
 
             Assert.IsType<BindingNotification>(result);
@@ -85,7 +85,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue()
         {
             var data = new Class1 { StringValue = null };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
             var result = await target.Take(1);
 
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
@@ -97,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Convert_Set_String_To_Double()
         {
             var data = new Class1 { StringValue = $"{5.6}" };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
 
             target.OnNext(6.7);
 
@@ -110,7 +110,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Convert_Get_Double_To_String()
         {
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
             var result = await target.Take(1);
 
             Assert.Equal($"{5.6}", result);
@@ -122,7 +122,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Convert_Set_Double_To_String()
         {
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
 
             target.OnNext($"{6.7}");
 
@@ -136,7 +136,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             var data = new Class1 { StringValue = "foo" };
             var target = new BindingExpression(
-                ExpressionObserverBuilder.Build(data, "StringValue"),
+                ExpressionObserver.Create(data, o => o.StringValue),
                 typeof(int),
                 42,
                 DefaultValueConverter.Instance);
@@ -157,7 +157,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             var data = new Class1 { StringValue = "foo" };
             var target = new BindingExpression(
-                ExpressionObserverBuilder.Build(data, "StringValue", true),
+                ExpressionObserver.Create(data, o => o.StringValue, true),
                 typeof(int),
                 42,
                 DefaultValueConverter.Instance);
@@ -178,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             var data = new Class1 { StringValue = "foo" };
             var target = new BindingExpression(
-                ExpressionObserverBuilder.Build(data, "StringValue"),
+                ExpressionObserver.Create(data, o => o.StringValue),
                 typeof(int),
                 "bar",
                 DefaultValueConverter.Instance);
@@ -200,7 +200,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             var data = new Class1 { StringValue = "foo" };
             var target = new BindingExpression(
-                ExpressionObserverBuilder.Build(data, "StringValue", true),
+                ExpressionObserver.Create(data, o => o.StringValue, true),
                 typeof(int),
                 "bar",
                 DefaultValueConverter.Instance);
@@ -221,7 +221,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Setting_Invalid_Double_String_Should_Not_Change_Target()
         {
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
 
             target.OnNext("foo");
 
@@ -235,7 +235,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             var data = new Class1 { DoubleValue = 5.6 };
             var target = new BindingExpression(
-                ExpressionObserverBuilder.Build(data, "DoubleValue"),
+                ExpressionObserver.Create(data, o => o.DoubleValue),
                 typeof(string),
                 "9.8",
                 DefaultValueConverter.Instance);
@@ -251,7 +251,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Coerce_Setting_Null_Double_To_Default_Value()
         {
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
 
             target.OnNext(null);
 
@@ -264,7 +264,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
         {
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
 
             target.OnNext(AvaloniaProperty.UnsetValue);
 
@@ -280,7 +280,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var converter = new Mock<IValueConverter>();
 
             var target = new BindingExpression(
-                ExpressionObserverBuilder.Build(data, "DoubleValue"),
+                ExpressionObserver.Create(data, o => o.DoubleValue),
                 typeof(string),
                 converter.Object,
                 converterParameter: "foo");
@@ -298,7 +298,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
             var target = new BindingExpression(
-                ExpressionObserverBuilder.Build(data, "DoubleValue"),
+                ExpressionObserver.Create(data, o => o.DoubleValue),
                 typeof(string),
                 converter.Object,
                 converterParameter: "foo");
@@ -315,7 +315,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
-            var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue", true), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue, true), typeof(string));
             var result = new List<object>();
 
             target.Subscribe(x => result.Add(x));

+ 5 - 42
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs

@@ -8,25 +8,17 @@ using System.Threading.Tasks;
 using Avalonia.Diagnostics;
 using Avalonia.Data.Core;
 using Xunit;
-using Avalonia.Markup.Parsers;
 
 namespace Avalonia.Base.UnitTests.Data.Core
 {
     public class ExpressionObserverTests_AttachedProperty
     {
-        private readonly Func<string, string, Type> _typeResolver;
-
-        public ExpressionObserverTests_AttachedProperty()
-        {
-            var foo = Owner.FooProperty;
-            _typeResolver = (_, name) => name == "Owner" ? typeof(Owner) : null;
-        }
 
         [Fact]
         public async Task Should_Get_Attached_Property_Value()
         {
             var data = new Class1();
-            var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver);
+            var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]);
             var result = await target.Take(1);
 
             Assert.Equal("foo", result);
@@ -34,19 +26,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
         }
 
-        [Fact]
-        public async Task Should_Get_Attached_Property_Value_With_Namespace()
-        {
-            var data = new Class1();
-            var target = ExpressionObserverBuilder.Build(
-                data,
-                "(NS:Owner.Foo)",
-                typeResolver: (ns, name) => ns == "NS" && name == "Owner" ? typeof(Owner) : null);
-            var result = await target.Take(1);
-            Assert.Equal("foo", result);
-            Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
-        }
-
         [Fact]
         public async Task Should_Get_Chained_Attached_Property_Value()
         {
@@ -58,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 }
             };
 
-            var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver);
+            var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]);
             var result = await target.Take(1);
 
             Assert.Equal("bar", result);
@@ -70,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_Simple_Attached_Value()
         {
             var data = new Class1();
-            var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver);
+            var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -94,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 }
             };
 
-            var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver);
+            var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -113,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
             {
                 var source = new Class1();
-                var target = ExpressionObserverBuilder.Build(source, "(Owner.Foo)", typeResolver: _typeResolver);
+                var target = ExpressionObserver.Create(source, o => o.Next[Owner.FooProperty]);
                 return Tuple.Create(target, new WeakReference(source));
             };
 
@@ -125,22 +104,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Assert.Null(result.Item2.Target);
         }
 
-        [Fact]
-        public void Should_Fail_With_Attached_Property_With_Only_1_Part()
-        {
-            var data = new Class1();
-
-            Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner)", typeResolver: _typeResolver));
-        }
-
-        [Fact]
-        public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts()
-        {
-            var data = new Class1();
-
-            Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver));
-        }
-
         private static class Owner
         {
             public static readonly AttachedProperty<string> FooProperty =

+ 6 - 4
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_Property_Value()
         {
             var data = new Class1();
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = await target.Take(1);
 
             Assert.Equal("foo", result);
@@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_ClrProperty_Value()
         {
             var data = new Class1();
-            var target = ExpressionObserverBuilder.Build(data, "ClrProperty");
+            var target = ExpressionObserver.Create(data, o => o.ClrProperty);
             var result = await target.Take(1);
 
             Assert.Equal("clr-property", result);
@@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_Simple_Property_Value()
         {
             var data = new Class1();
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -64,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
             {
                 var source = new Class1();
-                var target = ExpressionObserverBuilder.Build(source, "Foo");
+                var target = ExpressionObserver.Create(source, o => o.Foo);
                 return Tuple.Create(target, new WeakReference(source));
             };
 
@@ -81,6 +81,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
             public static readonly StyledProperty<string> FooProperty =
                 AvaloniaProperty.Register<Class1, string>("Foo", defaultValue: "foo");
 
+            public string Foo { get => GetValue(FooProperty); set => SetValue(FooProperty, value); }
+
             public string ClrProperty { get; } = "clr-property";
         }
     }

+ 12 - 19
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
         {
             var data = new ExceptionTest { MustBePositive = 5 };
-            var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), false);
+            var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
             var validationMessageFound = false;
 
             observer.OfType<BindingNotification>()
@@ -37,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Exception_Validation_Sends_DataValidationError()
         {
             var data = new ExceptionTest { MustBePositive = 5 };
-            var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true);
+            var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
             var validationMessageFound = false;
 
             observer.OfType<BindingNotification>()
@@ -54,7 +54,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled()
         {
             var data = new IndeiTest { MustBePositive = 5 };
-            var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), false);
+            var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
 
             observer.Subscribe(_ => { });
 
@@ -65,7 +65,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Enabled_Indei_Validation_Subscribes()
         {
             var data = new IndeiTest { MustBePositive = 5 };
-            var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true);
+            var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
             var sub = observer.Subscribe(_ => { });
 
             Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
@@ -77,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Validation_Plugins_Send_Correct_Notifications()
         {
             var data = new IndeiTest();
-            var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true);
+            var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
             var result = new List<object>();
             
             var errmsg = string.Empty;
@@ -123,10 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 Inner = new IndeiTest()
             };
 
-            var observer = ExpressionObserverBuilder.Build(
-                data,
-                $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
-                true);
+            var observer = ExpressionObserver.Create(data, o => o.Inner.MustBePositive, true);
 
             observer.Subscribe(_ => { });
 
@@ -134,19 +131,16 @@ namespace Avalonia.Base.UnitTests.Data.Core
             // intermediate object in a chain so for the moment I'm not sure what the result of 
             // validating such a thing should look like.
             Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
-            Assert.Equal(1, ((IndeiTest)data.Inner).ErrorsChangedSubscriptionCount);
+            Assert.Equal(1, data.Inner.ErrorsChangedSubscriptionCount);
         }
 
         [Fact]
         public void Sends_Correct_Notifications_With_Property_Chain()
         {
             var container = new Container();
-            var inner = new IndeiTest();
 
-            var observer = ExpressionObserverBuilder.Build(
-                container,
-                $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
-                true);
+            var observer = ExpressionObserver.Create(container, o => o.Inner.MustBePositive, true);
+
             var result = new List<object>();
 
             observer.Subscribe(x => result.Add(x));
@@ -154,13 +148,12 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Assert.Equal(new[]
             {
                 new BindingNotification(
-                    new MarkupBindingChainException("Null value", "Inner.MustBePositive", "Inner"),
+                    new MarkupBindingChainException("Null value", "o => o.Inner.MustBePositive", "Inner"),
                     BindingErrorType.Error,
                     AvaloniaProperty.UnsetValue),
             }, result);
 
             GC.KeepAlive(container);
-            GC.KeepAlive(inner);
         }
 
         public class ExceptionTest : NotifyingBase
@@ -221,9 +214,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
 
         private class Container : IndeiBase
         {
-            private object _inner;
+            private IndeiTest _inner;
 
-            public object Inner
+            public IndeiTest Inner
             {
                 get { return _inner; }
                 set { _inner = value; RaisePropertyChanged(); }

+ 16 - 0
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs

@@ -181,6 +181,17 @@ namespace Avalonia.Base.UnitTests.Data.Core
             }
         }
 
+        [Fact]
+        public async Task Should_Create_Method_Binding()
+        {
+            var data = new Class3();
+            var target = ExpressionObserver.Create(data, o => (Action)o.Method);
+            var value = await target.Take(1);
+
+            Assert.IsAssignableFrom<Delegate>(value);
+            GC.KeepAlive(data);
+        }
+
         private class Class1 : NotifyingBase
         {
             private string _foo;
@@ -204,5 +215,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
 
             public string ClrProperty { get; } = "clr-property";
         }
+
+        private class Class3
+        {
+            public void Method() { }
+        }
     }
 }

+ 20 - 68
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Array_Value()
         {
             var data = new { Foo = new [] { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, x => x.Foo[1]);
             var result = await target.Take(1);
 
             Assert.Equal("bar", result);
@@ -29,47 +29,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
         }
 
-        [Fact]
-        public async Task Should_Get_UnsetValue_For_Invalid_Array_Index()
-        {
-            var data = new { Foo = new[] { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]");
-            var result = await target.Take(1);
-
-            Assert.Equal(AvaloniaProperty.UnsetValue, result);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index()
-        {
-            var data = new { Foo = new Dictionary<int, string> { { 1, "foo" } } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]");
-            var result = await target.Take(1);
-
-            Assert.Equal(AvaloniaProperty.UnsetValue, result);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public async Task Should_Get_UnsetValue_For_Object_Without_Indexer()
-        {
-            var data = new { Foo = 5 };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[noindexer]");
-            var result = await target.Take(1);
-
-            Assert.Equal(AvaloniaProperty.UnsetValue, result);
-
-            GC.KeepAlive(data);
-        }
-
         [Fact]
         public async Task Should_Get_MultiDimensional_Array_Value()
         {
             var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[1, 1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1, 1]);
             var result = await target.Take(1);
 
             Assert.Equal("qux", result);
@@ -81,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Value_For_String_Indexer()
         {
             var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
             var result = await target.Take(1);
 
             Assert.Equal("bar", result);
@@ -93,7 +57,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Value_For_Non_String_Indexer()
         {
             var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[1.0]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1.0]);
             var result = await target.Take(1);
 
             Assert.Equal("bar", result);
@@ -105,19 +69,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
         {
             var data = new { Foo = new[] { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
-            var result = await target.Take(1);
-
-            Assert.Equal(AvaloniaProperty.UnsetValue, result);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue()
-        {
-            var data = new { Foo = new[] { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[1,2]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[2]);
             var result = await target.Take(1);
 
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
@@ -129,7 +81,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
         {
             var data = new { Foo = new List<string> { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[2]);
             var result = await target.Take(1);
 
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
@@ -141,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_List_Value()
         {
             var data = new { Foo = new List<string> { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
             var result = await target.Take(1);
 
             Assert.Equal("bar", result);
@@ -153,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Add()
         {
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[2]);
             var result = new List<object>();
 
             using (var sub = target.Subscribe(x => result.Add(x)))
@@ -171,7 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Remove()
         {
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[0]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[0]);
             var result = new List<object>();
 
             using (var sub = target.Subscribe(x => result.Add(x)))
@@ -189,7 +141,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Replace()
         {
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
             var result = new List<object>();
 
             using (var sub = target.Subscribe(x => result.Add(x)))
@@ -210,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             // method, but even if it did we need to test with ObservableCollection as well
             // as AvaloniaList as it implements PropertyChanged as an explicit interface event.
             var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -226,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Reset()
         {
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -245,7 +197,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             data.Foo["foo"] = "bar";
             data.Foo["baz"] = "qux";
 
-            var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
             var result = new List<object>();
 
             using (var sub = target.Subscribe(x => result.Add(x)))
@@ -264,7 +216,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_SetArrayIndex()
         {
             var data = new { Foo = new[] { "foo", "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
 
             using (target.Subscribe(_ => { }))
             {
@@ -286,8 +238,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     {"foo", 1 }
                 }
             };
-            
-            var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
+
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
             using (target.Subscribe(_ => { }))
             {
                 Assert.True(target.SetValue(4));
@@ -308,8 +260,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     {"foo", 1 }
                 }
             };
-            
-            var target = ExpressionObserverBuilder.Build(data, "Foo[bar]");
+
+            var target = ExpressionObserver.Create(data, o => o.Foo["bar"]);
             using (target.Subscribe(_ => { }))
             {
                 Assert.True(target.SetValue(4));
@@ -327,7 +279,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             data.Foo["foo"] = "bar";
             data.Foo["baz"] = "qux";
 
-            var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
 
             using (target.Subscribe(_ => { }))
             {
@@ -344,7 +296,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             var data = new[] { 1, 2, 3 };
 
-            var target = ExpressionObserverBuilder.Build(data, "[1]");
+            var target = ExpressionObserver.Create(data, o => o[1]);
 
             var value = await target.Take(1);
 

+ 8 - 8
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Complete_When_Source_Observable_Completes()
         {
             var source = new BehaviorSubject<object>(1);
-            var target = ExpressionObserverBuilder.Build(source, "Foo");
+            var target = ExpressionObserver.Create<object, object>(source, o => o);
             var completed = false;
 
             target.Subscribe(_ => { }, () => completed = true);
@@ -32,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Complete_When_Source_Observable_Errors()
         {
             var source = new BehaviorSubject<object>(1);
-            var target = ExpressionObserverBuilder.Build(source, "Foo");
+            var target = ExpressionObserver.Create<object, object>(source, o => o);
             var completed = false;
 
             target.Subscribe(_ => { }, () => completed = true);
@@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Complete_When_Update_Observable_Completes()
         {
             var update = new Subject<Unit>();
-            var target = ExpressionObserverBuilder.Build(() => 1, "Foo", update);
+            var target = ExpressionObserver.Create(() => 1, o => o, update);
             var completed = false;
 
             target.Subscribe(_ => { }, () => completed = true);
@@ -58,7 +58,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Complete_When_Update_Observable_Errors()
         {
             var update = new Subject<Unit>();
-            var target = ExpressionObserverBuilder.Build(() => 1, "Foo", update);
+            var target = ExpressionObserver.Create(() => 1, o => o, update);
             var completed = false;
 
             target.Subscribe(_ => { }, () => completed = true);
@@ -73,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var scheduler = new TestScheduler();
             var source = scheduler.CreateColdObservable(
                 OnNext(1, new { Foo = "foo" }));
-            var target = ExpressionObserverBuilder.Build(source, "Foo");
+            var target = ExpressionObserver.Create(source, o => o.Foo);
             var result = new List<object>();
 
             using (target.Subscribe(x => result.Add(x)))
@@ -92,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var scheduler = new TestScheduler();
             var update = scheduler.CreateColdObservable<Unit>();
             var data = new { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(() => data, "Foo", update);
+            var target = ExpressionObserver.Create(() => data, o => o.Foo, update);
             var result = new List<object>();
 
             using (target.Subscribe(x => result.Add(x)))
@@ -107,9 +107,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
         }
 
-        private Recorded<Notification<object>> OnNext(long time, object value)
+        private Recorded<Notification<T>> OnNext<T>(long time, T value)
         {
-            return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value));
+            return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
         }
     }
 }

+ 2 - 94
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Negate_Boolean_Value()
         {
             var data = new { Foo = true };
-            var target = ExpressionObserverBuilder.Build(data, "!Foo");
+            var target = ExpressionObserver.Create(data, o => !o.Foo);
             var result = await target.Take(1);
 
             Assert.False((bool)result);
@@ -25,103 +25,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
         }
 
-        [Fact]
-        public async Task Should_Negate_0()
-        {
-            var data = new { Foo = 0 };
-            var target = ExpressionObserverBuilder.Build(data, "!Foo");
-            var result = await target.Take(1);
-
-            Assert.True((bool)result);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public async Task Should_Negate_1()
-        {
-            var data = new { Foo = 1 };
-            var target = ExpressionObserverBuilder.Build(data, "!Foo");
-            var result = await target.Take(1);
-
-            Assert.False((bool)result);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public async Task Should_Negate_False_String()
-        {
-            var data = new { Foo = "false" };
-            var target = ExpressionObserverBuilder.Build(data, "!Foo");
-            var result = await target.Take(1);
-
-            Assert.True((bool)result);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public async Task Should_Negate_True_String()
-        {
-            var data = new { Foo = "True" };
-            var target = ExpressionObserverBuilder.Build(data, "!Foo");
-            var result = await target.Take(1);
-
-            Assert.False((bool)result);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
-        {
-            var data = new { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "!Foo");
-            var result = await target.Take(1);
-
-            Assert.Equal(
-                new BindingNotification(
-                    new InvalidCastException($"Unable to convert 'foo' to bool."),
-                    BindingErrorType.Error), 
-                result);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
-        {
-            var data = new { Foo = new object() };
-            var target = ExpressionObserverBuilder.Build(data, "!Foo");
-            var result = await target.Take(1);
-
-            Assert.Equal(
-                new BindingNotification(
-                    new InvalidCastException($"Unable to convert 'System.Object' to bool."),
-                    BindingErrorType.Error),
-                result);
-
-            GC.KeepAlive(data);
-        }
-
-        [Fact]
-        public void SetValue_Should_Return_False_For_Invalid_Value()
-        {
-            var data = new { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "!Foo");
-            target.Subscribe(_ => { });
-
-            Assert.False(target.SetValue("bar"));
-
-            GC.KeepAlive(data);
-        }
-
         [Fact]
         public void Can_SetValue_For_Valid_Value()
         {
             var data = new Test { Foo = true };
-            var target = ExpressionObserverBuilder.Build(data, "!Foo");
+            var target = ExpressionObserver.Create(data, o => !o.Foo);
             target.Subscribe(_ => { });
 
             Assert.True(target.SetValue(true));

+ 14 - 9
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs

@@ -16,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
     public class ExpressionObserverTests_Observable
     {
         [Fact]
-        public void Should_Not_Get_Observable_Value_Without_Modifier_Char()
+        public void Should_Not_Get_Observable_Value_Without_Streaming()
         {
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
                 var source = new BehaviorSubject<string>("foo");
                 var data = new { Foo = source };
-                var target = ExpressionObserverBuilder.Build(data, "Foo");
+                var target = ExpressionObserver.Create(data, o => o.Foo);
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
                 var source = new BehaviorSubject<string>("foo");
                 var data = new { Foo = source };
-                var target = ExpressionObserverBuilder.Build(data, "Foo^");
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -61,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
                 var data = new Class1();
-                var target = ExpressionObserverBuilder.Build(data, "Next^.Foo");
+                var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo);
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -84,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
                 var source = new BehaviorSubject<string>("foo");
                 var data = new { Foo = source };
-                var target = ExpressionObserverBuilder.Build(data, "Foo^", true);
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -106,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
                 var data1 = new Class1();
                 var data2 = new Class2("foo");
-                var target = ExpressionObserverBuilder.Build(data1, "Next^.Foo", true);
+                var target = ExpressionObserver.Create(data1, o => o.Next.StreamBinding().Foo, true);
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -128,8 +128,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
-                var data = new Class2("foo");
-                var target = ExpressionObserverBuilder.Build(data, "Foo^", true);
+                var data = new NotStreamable();
+                var target = ExpressionObserver.Create(data, o => o.StreamBinding());
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -139,7 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     new[]
                     {
                         new BindingNotification(
-                            new MarkupBindingChainException("Stream operator applied to unsupported type", "Foo^", "Foo^"),
+                            new MarkupBindingChainException("Stream operator applied to unsupported type", "o => o.StreamBinding()", "^"),
                             BindingErrorType.Error)
                     },
                     result);
@@ -164,5 +164,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
 
             public string Foo { get; }
         }
+
+        private class NotStreamable
+        {
+            public object StreamBinding() { throw new InvalidOperationException(); }
+        }
     }
 }

+ 40 - 79
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_Property_Value()
         {
             var data = new { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = await target.Take(1);
 
             Assert.Equal("foo", result);
@@ -34,7 +34,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Get_Simple_Property_Value_Type()
         {
             var data = new { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
 
             target.Subscribe(_ => { });
 
@@ -47,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_Property_Value_Null()
         {
             var data = new { Foo = (string)null };
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = await target.Take(1);
 
             Assert.Null(result);
@@ -59,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_Property_From_Base_Class()
         {
             var data = new Class3 { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = await target.Take(1);
 
             Assert.Equal("foo", result);
@@ -70,76 +70,65 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         public async Task Should_Return_BindingNotification_Error_For_Root_Null()
         {
-            var data = new Class3 { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(default(object), "Foo");
+            var target = ExpressionObserver.Create(default(Class3), o => o.Foo);
             var result = await target.Take(1);
 
             Assert.Equal(
                 new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                 result);
-
-            GC.KeepAlive(data);
         }
 
         [Fact]
         public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue()
         {
-            var data = new Class3 { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(AvaloniaProperty.UnsetValue, "Foo");
+            var target = ExpressionObserver.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo);
             var result = await target.Take(1);
 
             Assert.Equal(
                 new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                 result);
-
-            GC.KeepAlive(data);
         }
 
         [Fact]
         public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null()
         {
-            var data = new Class3 { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(Observable.Return(default(object)), "Foo");
+            var target = ExpressionObserver.Create(Observable.Return(default(Class3)), o => o.Foo);
             var result = await target.Take(1);
 
             Assert.Equal(
                 new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                 result);
-
-            GC.KeepAlive(data);
         }
 
         [Fact]
         public async void Should_Return_BindingNotification_Error_For_Observable_Root_UnsetValue()
         {
-            var data = new Class3 { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(Observable.Return(AvaloniaProperty.UnsetValue), "Foo");
+            var target = ExpressionObserver.Create<object, string>(Observable.Return(AvaloniaProperty.UnsetValue), o => (o as Class3).Foo);
             var result = await target.Take(1);
 
             Assert.Equal(
                 new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                 result);
-
-            GC.KeepAlive(data);
+            
         }
 
         [Fact]
         public async Task Should_Get_Simple_Property_Chain()
         {
             var data = new { Foo = new { Bar = new { Baz = "baz" } } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
+            var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
             var result = await target.Take(1);
 
             Assert.Equal("baz", result);
@@ -151,7 +140,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Get_Simple_Property_Chain_Type()
         {
             var data = new { Foo = new { Bar = new { Baz = "baz" } } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
+            var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
 
             target.Subscribe(_ => { });
 
@@ -160,28 +149,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
         }
 
-        [Fact]
-        public async Task Should_Return_BindingNotification_Error_For_Broken_Chain()
-        {
-            var data = new { Foo = new { Bar = 1 } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
-            var result = await target.Take(1);
-
-            Assert.IsType<BindingNotification>(result);
-
-            Assert.Equal(
-                new BindingNotification(
-                    new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
-                result);
-
-            GC.KeepAlive(data);
-        }
-
         [Fact]
         public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
         {
-            var data = new { Foo = default(object) };
-            var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
+            var data = new { Foo = default(Class1) };
+            var target = ExpressionObserver.Create(data, o => o.Foo.Foo.Length);
             var result = new List<object>();
 
             target.Subscribe(x => result.Add(x));
@@ -190,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 new[]
                 {
                     new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo.Bar.Baz", "Foo"),
+                        new MarkupBindingChainException("Null value", "o => o.Foo.Foo.Length", "Foo"),
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                 },
@@ -199,22 +171,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
         }
 
-        [Fact]
-        public void Should_Have_Null_ResultType_For_Broken_Chain()
-        {
-            var data = new { Foo = new { Bar = 1 } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
-
-            Assert.Null(target.ResultType);
-
-            GC.KeepAlive(data);
-        }
-
         [Fact]
         public void Should_Track_Simple_Property_Value()
         {
             var data = new Class1 { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -233,7 +194,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String()
         {
             var data = new Class1 { Bar = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "Bar");
+            var target = ExpressionObserver.Create(data, o => o.Bar);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -263,7 +224,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_End_Of_Property_Chain_Changing()
         {
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -284,7 +245,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_Property_Chain_Changing()
         {
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -317,7 +278,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 }
             };
 
-            var target = ExpressionObserverBuilder.Build(data, "Next.Next.Bar");
+            var target = ExpressionObserver.Create(data, o => ((o.Next as Class2).Next as Class2).Bar);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -330,7 +291,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 {
                     "bar",
                     new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Next.Next.Bar", "Next.Next"),
+                        new MarkupBindingChainException("Null value", "o => ((o.Next As Class2).Next As Class2).Bar", "Next.Next"),
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                     "bar"
@@ -350,7 +311,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
         {
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
             var result = new List<object>();
 
             var sub = target.Subscribe(x => result.Add(x));
@@ -385,7 +346,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             var data = new Class1 { Foo = "foo" };
             var update = new Subject<Unit>();
-            var target = ExpressionObserverBuilder.Build(() => data.Foo, "", update);
+            var target = ExpressionObserver.Create(() => data.Foo, o => o, update);
             var result = new List<object>();
 
             target.Subscribe(x => result.Add(x));
@@ -405,7 +366,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var source = scheduler.CreateColdObservable(
                 OnNext(1, new Class1 { Foo = "foo" }),
                 OnNext(2, new Class1 { Foo = "bar" }));
-            var target = ExpressionObserverBuilder.Build(source, "Foo");
+            var target = ExpressionObserver.Create(source, o => o.Foo);
             var result = new List<object>();
 
             using (target.Subscribe(x => result.Add(x)))
@@ -421,7 +382,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Subscribing_Multiple_Times_Should_Return_Values_To_All()
         {
             var data = new Class1 { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result1 = new List<object>();
             var result2 = new List<object>();
             var result3 = new List<object>();
@@ -444,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
         {
             var data = new Class1 { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
 
             var sub1 = target.Subscribe(x => { });
             var sub2 = target.Subscribe(x => { });
@@ -463,7 +424,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Set_Simple_Property_Value()
         {
             var data = new Class1 { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
 
             using (target.Subscribe(_ => { }))
             {
@@ -479,7 +440,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Set_Property_At_The_End_Of_Chain()
         {
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
 
             using (target.Subscribe(_ => { }))
             {
@@ -495,7 +456,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Return_False_For_Missing_Property()
         {
             var data = new Class1 { Next = new WithoutBar() };
-            var target = ExpressionObserverBuilder.Build(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
 
             using (target.Subscribe(_ => { }))
             {
@@ -509,7 +470,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Notify_New_Value_With_Inpc()
         {
             var data = new Class1();
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = new List<object>();
 
             target.Subscribe(x => result.Add(x));
@@ -524,7 +485,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Notify_New_Value_Without_Inpc()
         {
             var data = new Class1();
-            var target = ExpressionObserverBuilder.Build(data, "Bar");
+            var target = ExpressionObserver.Create(data, o => o.Bar);
             var result = new List<object>();
 
             target.Subscribe(x => result.Add(x));
@@ -539,7 +500,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Return_False_For_Missing_Object()
         {
             var data = new Class1();
-            var target = ExpressionObserverBuilder.Build(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
 
             using (target.Subscribe(_ => { }))
             {
@@ -556,7 +517,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var second = new Class1 { Foo = "bar" };
             var root = first;
             var update = new Subject<Unit>();
-            var target = ExpressionObserverBuilder.Build(() => root, "Foo", update);
+            var target = ExpressionObserver.Create(() => root, o => o.Foo, update);
             var result = new List<object>();
             var sub = target.Subscribe(x => result.Add(x));
 
@@ -571,7 +532,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     "foo",
                     "bar",
                     new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue)
                 },
@@ -590,7 +551,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
             {
                 var source = new Class1 { Foo = "foo" };
-                var target = ExpressionObserverBuilder.Build(source, "Foo");
+                var target = ExpressionObserver.Create(source, o => o.Foo);
                 return Tuple.Create(target, new WeakReference(source));
             };
 
@@ -674,9 +635,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         }
 
-        private Recorded<Notification<object>> OnNext(long time, object value)
+        private Recorded<Notification<T>> OnNext<T>(long time, T value)
         {
-            return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value));
+            return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
         }
     }
 }

+ 16 - 12
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Set_Simple_Property_Value()
         {
             var data = new { Foo = "foo" };
-            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
 
             using (target.Subscribe(_ => { }))
             {
@@ -31,7 +31,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Set_Value_On_Simple_Property_Chain()
         {
             var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo.Bar");
+            var target = ExpressionObserver.Create(data, o => o.Foo.Bar);
+
 
             using (target.Subscribe(_ => { }))
             {
@@ -45,14 +46,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Not_Try_To_Set_Value_On_Broken_Chain()
         {
             var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
-            var target = ExpressionObserverBuilder.Build(data, "Foo.Bar");
+            var target = ExpressionObserver.Create(data, o => o.Foo.Bar);
 
             // Ensure the ExpressionObserver's subscriptions are kept active.
-            target.OfType<string>().Subscribe(x => { });
-
-            data.Foo = null;
+            using (target.OfType<string>().Subscribe(x => { }))
+            {
+                data.Foo = null;
+                Assert.False(target.SetValue("foo"));
+            }
 
-            Assert.False(target.SetValue("foo"));
         }
 
         /// <summary>
@@ -68,13 +70,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
             var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
             var rootObservable = new BehaviorSubject<Class1>(data);
-            var target = ExpressionObserverBuilder.Build(rootObservable, "Foo.Bar");
+            var target = ExpressionObserver.Create(rootObservable, o => o.Foo.Bar);
 
-            target.Subscribe(_ => { });
-            rootObservable.OnNext(null);
-            target.SetValue("baz");
+            using (target.Subscribe(_ => { }))
+            {
+                rootObservable.OnNext(null);
+                target.SetValue("baz");
+                Assert.Equal("bar", data.Foo.Bar);
+            }
 
-            Assert.Equal("bar", data.Foo.Bar);
         }
 
         private class Class1 : NotifyingBase

+ 7 - 7
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs

@@ -16,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
     public class ExpressionObserverTests_Task
     {
         [Fact]
-        public void Should_Not_Get_Task_Result_Without_Modifier_Char()
+        public void Should_Not_Get_Task_Result_Without_StreamBinding()
         {
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
                 var tcs = new TaskCompletionSource<string>();
                 var data = new { Foo = tcs.Task };
-                var target = ExpressionObserverBuilder.Build(data, "Foo");
+                var target = ExpressionObserver.Create(data, o => o.Foo);
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
                 var data = new { Foo = Task.FromResult("foo") };
-                var target = ExpressionObserverBuilder.Build(data, "Foo^");
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -60,7 +60,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
                 var tcs = new TaskCompletionSource<Class2>();
                 var data = new Class1(tcs.Task);
-                var target = ExpressionObserverBuilder.Build(data, "Next^.Foo");
+                var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo);
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -80,7 +80,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
                 var tcs = new TaskCompletionSource<string>();
                 var data = new { Foo = tcs.Task };
-                var target = ExpressionObserverBuilder.Build(data, "Foo^");
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -106,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
                 var data = new { Foo = TaskFromException(new NotSupportedException()) };
-                var target = ExpressionObserverBuilder.Build(data, "Foo^");
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));
@@ -131,7 +131,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
                 var tcs = new TaskCompletionSource<string>();
                 var data = new { Foo = tcs.Task };
-                var target = ExpressionObserverBuilder.Build(data, "Foo^", true);
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
                 var result = new List<object>();
 
                 var sub = target.Subscribe(x => result.Add(x));

+ 9 - 0
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs

@@ -137,6 +137,15 @@ namespace Avalonia.Markup.UnitTests.Parsers
             AssertIsProperty(result[3], "Baz");
         }
 
+        [Fact]
+        public void Should_Build_Stream_Node()
+        {
+            var result = ToList(ExpressionObserverBuilder.Parse("Foo^"));
+
+            Assert.Equal(2, result.Count);
+            Assert.IsType<StreamNode>(result[1]);
+        }
+
         private void AssertIsProperty(ExpressionNode node, string name)
         {
             Assert.IsType<PropertyAccessorNode>(node);

+ 165 - 0
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs

@@ -0,0 +1,165 @@
+// 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;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Avalonia.Diagnostics;
+using Avalonia.Data.Core;
+using Xunit;
+using Avalonia.Markup.Parsers;
+
+namespace Avalonia.Markup.UnitTests.Parsers
+{
+    public class ExpressionObserverBuilderTests_AttachedProperty
+    {
+        private readonly Func<string, string, Type> _typeResolver;
+
+        public ExpressionObserverBuilderTests_AttachedProperty()
+        {
+            var foo = Owner.FooProperty;
+            _typeResolver = (_, name) => name == "Owner" ? typeof(Owner) : null;
+        }
+
+        [Fact]
+        public async Task Should_Get_Attached_Property_Value()
+        {
+            var data = new Class1();
+            var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver);
+            var result = await target.Take(1);
+
+            Assert.Equal("foo", result);
+
+            Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
+        }
+
+        [Fact]
+        public async Task Should_Get_Attached_Property_Value_With_Namespace()
+        {
+            var data = new Class1();
+            var target = ExpressionObserverBuilder.Build(
+                data,
+                "(NS:Owner.Foo)",
+                typeResolver: (ns, name) => ns == "NS" && name == "Owner" ? typeof(Owner) : null);
+            var result = await target.Take(1);
+            Assert.Equal("foo", result);
+            Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
+        }
+
+        [Fact]
+        public async Task Should_Get_Chained_Attached_Property_Value()
+        {
+            var data = new Class1
+            {
+                Next = new Class1
+                {
+                    [Owner.FooProperty] = "bar",
+                }
+            };
+
+            var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver);
+            var result = await target.Take(1);
+
+            Assert.Equal("bar", result);
+
+            Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
+        }
+
+        [Fact]
+        public void Should_Track_Simple_Attached_Value()
+        {
+            var data = new Class1();
+            var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver);
+            var result = new List<object>();
+
+            var sub = target.Subscribe(x => result.Add(x));
+            data.SetValue(Owner.FooProperty, "bar");
+
+            Assert.Equal(new[] { "foo", "bar" }, result);
+
+            sub.Dispose();
+
+            Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
+        }
+
+        [Fact]
+        public void Should_Track_Chained_Attached_Value()
+        {
+            var data = new Class1
+            {
+                Next = new Class1
+                {
+                    [Owner.FooProperty] = "foo",
+                }
+            };
+
+            var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver);
+            var result = new List<object>();
+
+            var sub = target.Subscribe(x => result.Add(x));
+            data.Next.SetValue(Owner.FooProperty, "bar");
+
+            Assert.Equal(new[] { "foo", "bar" }, result);
+
+            sub.Dispose();
+
+            Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
+        }
+
+        [Fact]
+        public void Should_Not_Keep_Source_Alive()
+        {
+            Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
+            {
+                var source = new Class1();
+                var target = ExpressionObserverBuilder.Build(source, "(Owner.Foo)", typeResolver: _typeResolver);
+                return Tuple.Create(target, new WeakReference(source));
+            };
+
+            var result = run();
+            result.Item1.Subscribe(x => { });
+
+            GC.Collect();
+
+            Assert.Null(result.Item2.Target);
+        }
+
+        [Fact]
+        public void Should_Fail_With_Attached_Property_With_Only_1_Part()
+        {
+            var data = new Class1();
+
+            Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner)", typeResolver: _typeResolver));
+        }
+
+        [Fact]
+        public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts()
+        {
+            var data = new Class1();
+
+            Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver));
+        }
+
+        private static class Owner
+        {
+            public static readonly AttachedProperty<string> FooProperty =
+                AvaloniaProperty.RegisterAttached<Class1, string>(
+                    "Foo", 
+                    typeof(Owner), 
+                    defaultValue: "foo");
+        }
+
+        private class Class1 : AvaloniaObject
+        {
+            public static readonly StyledProperty<Class1> NextProperty =
+                AvaloniaProperty.Register<Class1, Class1>(nameof(Next));
+
+            public Class1 Next
+            {
+                get { return GetValue(NextProperty); }
+                set { SetValue(NextProperty, value); }
+            }
+        }
+    }
+}

+ 59 - 0
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs

@@ -0,0 +1,59 @@
+// 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;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Avalonia.Diagnostics;
+using Avalonia.Data.Core;
+using Xunit;
+using Avalonia.Markup.Parsers;
+
+namespace Avalonia.Markup.UnitTests.Parsers
+{
+    public class ExpressionObserverBuilderTests_AvaloniaProperty
+    {
+        public ExpressionObserverBuilderTests_AvaloniaProperty()
+        {
+            var foo = Class1.FooProperty;
+        }
+
+        [Fact]
+        public async Task Should_Get_AvaloniaProperty_By_Name()
+        {
+            var data = new Class1();
+            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var result = await target.Take(1);
+
+            Assert.Equal("foo", result);
+
+            Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
+        }
+
+        [Fact]
+        public void Should_Track_AvaloniaProperty_By_Name()
+        {
+            var data = new Class1();
+            var target = ExpressionObserverBuilder.Build(data, "Foo");
+            var result = new List<object>();
+
+            var sub = target.Subscribe(x => result.Add(x));
+            data.SetValue(Class1.FooProperty, "bar");
+
+            Assert.Equal(new[] { "foo", "bar" }, result);
+
+            sub.Dispose();
+
+            Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
+        }
+
+        private class Class1 : AvaloniaObject
+        {
+            public static readonly StyledProperty<string> FooProperty =
+                AvaloniaProperty.Register<Class1, string>("Foo", defaultValue: "foo");
+
+            public string ClrProperty { get; } = "clr-property";
+        }
+    }
+}

+ 371 - 0
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs

@@ -0,0 +1,371 @@
+using Avalonia.Collections;
+using Avalonia.Data.Core;
+using Avalonia.Diagnostics;
+using Avalonia.Markup.Parsers;
+using Avalonia.Markup.Parsers.Nodes;
+using Avalonia.UnitTests;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Reactive.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Parsers
+{
+    public class ExpressionObserverBuilderTests_Indexer
+    {
+        [Fact]
+        public async Task Should_Get_Array_Value()
+        {
+            var data = new { Foo = new[] { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var result = await target.Take(1);
+
+            Assert.Equal("bar", result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Get_UnsetValue_For_Invalid_Array_Index()
+        {
+            var data = new { Foo = new[] { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]");
+            var result = await target.Take(1);
+
+            Assert.Equal(AvaloniaProperty.UnsetValue, result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index()
+        {
+            var data = new { Foo = new Dictionary<int, string> { { 1, "foo" } } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]");
+            var result = await target.Take(1);
+
+            Assert.Equal(AvaloniaProperty.UnsetValue, result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Get_UnsetValue_For_Object_Without_Indexer()
+        {
+            var data = new { Foo = 5 };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[noindexer]");
+            var result = await target.Take(1);
+
+            Assert.Equal(AvaloniaProperty.UnsetValue, result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Get_MultiDimensional_Array_Value()
+        {
+            var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[1, 1]");
+            var result = await target.Take(1);
+
+            Assert.Equal("qux", result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Get_Value_For_String_Indexer()
+        {
+            var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
+            var result = await target.Take(1);
+
+            Assert.Equal("bar", result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Get_Value_For_Non_String_Indexer()
+        {
+            var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[1.0]");
+            var result = await target.Take(1);
+
+            Assert.Equal("bar", result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
+        {
+            var data = new { Foo = new[] { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
+            var result = await target.Take(1);
+
+            Assert.Equal(AvaloniaProperty.UnsetValue, result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue()
+        {
+            var data = new { Foo = new[] { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[1,2]");
+            var result = await target.Take(1);
+
+            Assert.Equal(AvaloniaProperty.UnsetValue, result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
+        {
+            var data = new { Foo = new List<string> { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
+            var result = await target.Take(1);
+
+            Assert.Equal(AvaloniaProperty.UnsetValue, result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Get_List_Value()
+        {
+            var data = new { Foo = new List<string> { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var result = await target.Take(1);
+
+            Assert.Equal("bar", result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Track_INCC_Add()
+        {
+            var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[2]");
+            var result = new List<object>();
+
+            using (var sub = target.Subscribe(x => result.Add(x)))
+            {
+                data.Foo.Add("baz");
+            }
+
+            Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result);
+            Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Track_INCC_Remove()
+        {
+            var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[0]");
+            var result = new List<object>();
+
+            using (var sub = target.Subscribe(x => result.Add(x)))
+            {
+                data.Foo.RemoveAt(0);
+            }
+
+            Assert.Equal(new[] { "foo", "bar" }, result);
+            Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Track_INCC_Replace()
+        {
+            var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var result = new List<object>();
+
+            using (var sub = target.Subscribe(x => result.Add(x)))
+            {
+                data.Foo[1] = "baz";
+            }
+
+            Assert.Equal(new[] { "bar", "baz" }, result);
+            Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers());
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Track_INCC_Move()
+        {
+            // Using ObservableCollection here because AvaloniaList does not yet have a Move
+            // method, but even if it did we need to test with ObservableCollection as well
+            // as AvaloniaList as it implements PropertyChanged as an explicit interface event.
+            var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var result = new List<object>();
+
+            var sub = target.Subscribe(x => result.Add(x));
+            data.Foo.Move(0, 1);
+
+            Assert.Equal(new[] { "bar", "foo" }, result);
+
+            GC.KeepAlive(sub);
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Track_INCC_Reset()
+        {
+            var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+            var result = new List<object>();
+
+            var sub = target.Subscribe(x => result.Add(x));
+            data.Foo.Clear();
+
+            Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue }, result);
+
+            GC.KeepAlive(sub);
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Track_NonIntegerIndexer()
+        {
+            var data = new { Foo = new NonIntegerIndexer() };
+            data.Foo["foo"] = "bar";
+            data.Foo["baz"] = "qux";
+
+            var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
+            var result = new List<object>();
+
+            using (var sub = target.Subscribe(x => result.Add(x)))
+            {
+                data.Foo["foo"] = "bar2";
+            }
+
+            var expected = new[] { "bar", "bar2" };
+            Assert.Equal(expected, result);
+            Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_SetArrayIndex()
+        {
+            var data = new { Foo = new[] { "foo", "bar" } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo[1]");
+
+            using (target.Subscribe(_ => { }))
+            {
+                Assert.True(target.SetValue("baz"));
+            }
+
+            Assert.Equal("baz", data.Foo[1]);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Set_ExistingDictionaryEntry()
+        {
+            var data = new
+            {
+                Foo = new Dictionary<string, int>
+                {
+                    {"foo", 1 }
+                }
+            };
+
+            var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
+            using (target.Subscribe(_ => { }))
+            {
+                Assert.True(target.SetValue(4));
+            }
+
+            Assert.Equal(4, data.Foo["foo"]);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Add_NewDictionaryEntry()
+        {
+            var data = new
+            {
+                Foo = new Dictionary<string, int>
+                {
+                    {"foo", 1 }
+                }
+            };
+
+            var target = ExpressionObserverBuilder.Build(data, "Foo[bar]");
+            using (target.Subscribe(_ => { }))
+            {
+                Assert.True(target.SetValue(4));
+            }
+
+            Assert.Equal(4, data.Foo["bar"]);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Set_NonIntegerIndexer()
+        {
+            var data = new { Foo = new NonIntegerIndexer() };
+            data.Foo["foo"] = "bar";
+            data.Foo["baz"] = "qux";
+
+            var target = ExpressionObserverBuilder.Build(data, "Foo[foo]");
+
+            using (target.Subscribe(_ => { }))
+            {
+                Assert.True(target.SetValue("bar2"));
+            }
+
+            Assert.Equal("bar2", data.Foo["foo"]);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Indexer_Only_Binding_Works()
+        {
+            var data = new[] { 1, 2, 3 };
+
+            var target = ExpressionObserverBuilder.Build(data, "[1]");
+
+            var value = await target.Take(1);
+
+            Assert.Equal(data[1], value);
+        }
+
+        private class NonIntegerIndexer : NotifyingBase
+        {
+            private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();
+
+            public string this[string key]
+            {
+                get
+                {
+                    return _storage[key];
+                }
+                set
+                {
+                    _storage[key] = value;
+                    RaisePropertyChanged(CommonPropertyNames.IndexerName);
+                }
+            }
+        }
+    }
+}

+ 2 - 2
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs

@@ -9,9 +9,9 @@ using System.Text;
 using System.Threading.Tasks;
 using Xunit;
 
-namespace Avalonia.Base.UnitTests.Data.Core
+namespace Avalonia.Markup.UnitTests.Parsers
 {
-    public class ExpressionObserverTests_Method
+    public class ExpressionObserverBuilderTests_Method
     {
         private class TestObject
         {

+ 112 - 0
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs

@@ -0,0 +1,112 @@
+// 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;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Avalonia.Data;
+using Avalonia.Markup.Parsers;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Parsers
+{
+    public class ExpressionObserverBuilderTests_Negation
+    {
+        [Fact]
+        public async Task Should_Negate_0()
+        {
+            var data = new { Foo = 0 };
+            var target = ExpressionObserverBuilder.Build(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.True((bool)result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Negate_1()
+        {
+            var data = new { Foo = 1 };
+            var target = ExpressionObserverBuilder.Build(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.False((bool)result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Negate_False_String()
+        {
+            var data = new { Foo = "false" };
+            var target = ExpressionObserverBuilder.Build(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.True((bool)result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Negate_True_String()
+        {
+            var data = new { Foo = "True" };
+            var target = ExpressionObserverBuilder.Build(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.False((bool)result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
+        {
+            var data = new { Foo = "foo" };
+            var target = ExpressionObserverBuilder.Build(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.Equal(
+                new BindingNotification(
+                    new InvalidCastException($"Unable to convert 'foo' to bool."),
+                    BindingErrorType.Error), 
+                result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
+        {
+            var data = new { Foo = new object() };
+            var target = ExpressionObserverBuilder.Build(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.Equal(
+                new BindingNotification(
+                    new InvalidCastException($"Unable to convert 'System.Object' to bool."),
+                    BindingErrorType.Error),
+                result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void SetValue_Should_Return_False_For_Invalid_Value()
+        {
+            var data = new { Foo = "foo" };
+            var target = ExpressionObserverBuilder.Build(data, "!Foo");
+            target.Subscribe(_ => { });
+
+            Assert.False(target.SetValue("bar"));
+
+            GC.KeepAlive(data);
+        }
+
+        private class Test
+        {
+            public bool Foo { get; set; }
+        }
+    }
+}

+ 42 - 0
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs

@@ -0,0 +1,42 @@
+using Avalonia.Data;
+using Avalonia.Markup.Parsers;
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Parsers
+{
+    public class ExpressionObserverBuilderTests_Property
+    {
+        [Fact]
+        public async Task Should_Return_BindingNotification_Error_For_Broken_Chain()
+        {
+            var data = new { Foo = new { Bar = 1 } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
+            var result = await target.Take(1);
+
+            Assert.IsType<BindingNotification>(result);
+
+            Assert.Equal(
+                new BindingNotification(
+                    new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
+                result);
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Should_Have_Null_ResultType_For_Broken_Chain()
+        {
+            var data = new { Foo = new { Bar = 1 } };
+            var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz");
+
+            Assert.Null(target.ResultType);
+
+            GC.KeepAlive(data);
+        }
+    }
+}