소스 검색

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 년 전
부모
커밋
1b2d644e48
23개의 변경된 파일959개의 추가작업 그리고 379개의 파일을 삭제
  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);
+        }
+    }
+}