Browse Source

Merge pull request #1667 from jkoritzinsky/linq-expression-expressionobserver

Allow LINQ Expressions for Binding Expressions
Jeremy Koritzinsky 7 years ago
parent
commit
3dd5e3a4c2
52 changed files with 1936 additions and 596 deletions
  1. 60 0
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  2. 1 1
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  3. 1 1
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  4. 0 30
      src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs
  5. 89 41
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  6. 2 2
      src/Avalonia.Base/Data/Core/ExpressionParseException.cs
  7. 71 0
      src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs
  8. 92 0
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  9. 1 1
      src/Avalonia.Base/Data/Core/LogicalNotNode.cs
  10. 27 0
      src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs
  11. 219 0
      src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs
  12. 1 29
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  13. 1 1
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  14. 1 1
      src/Avalonia.Base/Data/Core/SettableNode.cs
  15. 27 0
      src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs
  16. 1 1
      src/Avalonia.Base/Data/Core/StreamNode.cs
  17. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs
  18. 1 0
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  19. 2 1
      src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
  20. 2 1
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  21. 23 12
      src/Markup/Avalonia.Markup/Data/Binding.cs
  22. 2 1
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  23. 75 0
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  24. 29 6
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  25. 1 1
      src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs
  26. 14 74
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs
  27. 1 1
      src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs
  28. 2 2
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  29. 22 21
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  30. 5 25
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs
  31. 7 4
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs
  32. 13 19
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  33. 224 0
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs
  34. 21 68
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs
  35. 9 8
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs
  36. 3 94
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs
  37. 15 9
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs
  38. 41 79
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  39. 17 12
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs
  40. 8 7
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs
  41. 1 1
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  42. 4 4
      tests/Avalonia.LeakTests/ExpressionObserverTests.cs
  43. 29 18
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs
  44. 12 11
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs
  45. 165 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs
  46. 59 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs
  47. 371 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs
  48. 7 6
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs
  49. 112 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs
  50. 42 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs
  51. 1 1
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  52. 1 1
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs

+ 60 - 0
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Text;
+using Avalonia.Reactive;
+
+namespace Avalonia.Data.Core
+{
+    public class AvaloniaPropertyAccessorNode : SettableNode
+    {
+        private IDisposable _subscription;
+        private readonly bool _enableValidation;
+        private readonly AvaloniaProperty _property;
+
+        public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation)
+        {
+            _property = property;
+            _enableValidation = enableValidation;
+        }
+
+        public override string Description => PropertyName;
+        public string PropertyName { get; }
+        public override Type PropertyType => _property.PropertyType;
+
+        protected override bool SetTargetValueCore(object value, BindingPriority priority)
+        {
+            try
+            {
+                if (Target.IsAlive && Target.Target is IAvaloniaObject obj)
+                {
+                    obj.SetValue(_property, value, priority);
+                    return true;
+                }
+                return false;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+        protected override void StartListeningCore(WeakReference reference)
+        {
+            if (reference.Target is IAvaloniaObject obj)
+            {
+                _subscription = new AvaloniaPropertyObservable<object>(obj, _property).Subscribe(ValueChanged);
+            }
+            else
+            {
+                _subscription = null;
+            }
+        }
+
+        protected override void StopListeningCore()
+        {
+            _subscription?.Dispose();
+            _subscription = null;
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs

@@ -6,7 +6,7 @@ using System.Reactive.Linq;
 
 
 namespace Avalonia.Data.Core
 namespace Avalonia.Data.Core
 {
 {
-    internal class EmptyExpressionNode : ExpressionNode
+    public class EmptyExpressionNode : ExpressionNode
     {
     {
         public override string Description => ".";
         public override string Description => ".";
     }
     }

+ 1 - 1
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@@ -5,7 +5,7 @@ using System;
 
 
 namespace Avalonia.Data.Core
 namespace Avalonia.Data.Core
 {
 {
-    internal abstract class ExpressionNode
+    public abstract class ExpressionNode
     {
     {
         private static readonly object CacheInvalid = new object();
         private static readonly object CacheInvalid = new object();
         protected static readonly WeakReference UnsetReference = 
         protected static readonly WeakReference UnsetReference = 

+ 0 - 30
src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs

@@ -1,30 +0,0 @@
-// 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 Avalonia.Data.Core.Parsers;
-
-namespace Avalonia.Data.Core
-{
-    internal static class ExpressionNodeBuilder
-    {
-        public static ExpressionNode Build(string expression, bool enableValidation = false)
-        {
-            if (string.IsNullOrWhiteSpace(expression))
-            {
-                throw new ArgumentException("'expression' may not be empty.");
-            }
-
-            var reader = new Reader(expression);
-            var parser = new ExpressionParser(enableValidation);
-            var node = parser.Parse(reader);
-
-            if (!reader.End)
-            {
-                throw new ExpressionParseException(reader.Position, "Expected end of expression.");
-            }
-
-            return node;
-        }
-    }
-}

+ 89 - 41
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@@ -3,9 +3,11 @@
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq.Expressions;
 using System.Reactive;
 using System.Reactive;
 using System.Reactive.Linq;
 using System.Reactive.Linq;
 using Avalonia.Data;
 using Avalonia.Data;
+using Avalonia.Data.Core.Parsers;
 using Avalonia.Data.Core.Plugins;
 using Avalonia.Data.Core.Plugins;
 using Avalonia.Reactive;
 using Avalonia.Reactive;
 
 
@@ -61,27 +63,22 @@ namespace Avalonia.Data.Core
         /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
         /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
         /// </summary>
         /// </summary>
         /// <param name="root">The root object.</param>
         /// <param name="root">The root object.</param>
-        /// <param name="expression">The expression.</param>
-        /// <param name="enableDataValidation">Whether data validation should be enabled.</param>
+        /// <param name="node">The expression.</param>
         /// <param name="description">
         /// <param name="description">
-        /// A description of the expression. If null, <paramref name="expression"/> will be used.
+        /// A description of the expression.
         /// </param>
         /// </param>
         public ExpressionObserver(
         public ExpressionObserver(
             object root,
             object root,
-            string expression,
-            bool enableDataValidation = false,
+            ExpressionNode node,
             string description = null)
             string description = null)
         {
         {
-            Contract.Requires<ArgumentNullException>(expression != null);
-
             if (root == AvaloniaProperty.UnsetValue)
             if (root == AvaloniaProperty.UnsetValue)
             {
             {
                 root = null;
                 root = null;
             }
             }
 
 
-            Expression = expression;
-            Description = description ?? expression;
-            _node = Parse(expression, enableDataValidation);
+            _node = node;
+            Description = description;
             _root = new WeakReference(root);
             _root = new WeakReference(root);
         }
         }
 
 
@@ -89,23 +86,19 @@ namespace Avalonia.Data.Core
         /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
         /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
         /// </summary>
         /// </summary>
         /// <param name="rootObservable">An observable which provides the root object.</param>
         /// <param name="rootObservable">An observable which provides the root object.</param>
-        /// <param name="expression">The expression.</param>
-        /// <param name="enableDataValidation">Whether data validation should be enabled.</param>
+        /// <param name="node">The expression.</param>
         /// <param name="description">
         /// <param name="description">
-        /// A description of the expression. If null, <paramref name="expression"/> will be used.
+        /// A description of the expression.
         /// </param>
         /// </param>
         public ExpressionObserver(
         public ExpressionObserver(
             IObservable<object> rootObservable,
             IObservable<object> rootObservable,
-            string expression,
-            bool enableDataValidation = false,
-            string description = null)
+            ExpressionNode node,
+            string description)
         {
         {
             Contract.Requires<ArgumentNullException>(rootObservable != null);
             Contract.Requires<ArgumentNullException>(rootObservable != null);
-            Contract.Requires<ArgumentNullException>(expression != null);
-
-            Expression = expression;
-            Description = description ?? expression;
-            _node = Parse(expression, enableDataValidation);
+            
+            _node = node;
+            Description = description;
             _root = rootObservable;
             _root = rootObservable;
         }
         }
 
 
@@ -113,30 +106,92 @@ namespace Avalonia.Data.Core
         /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
         /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
         /// </summary>
         /// </summary>
         /// <param name="rootGetter">A function which gets the root object.</param>
         /// <param name="rootGetter">A function which gets the root object.</param>
-        /// <param name="expression">The expression.</param>
+        /// <param name="node">The expression.</param>
         /// <param name="update">An observable which triggers a re-read of the getter.</param>
         /// <param name="update">An observable which triggers a re-read of the getter.</param>
-        /// <param name="enableDataValidation">Whether data validation should be enabled.</param>
         /// <param name="description">
         /// <param name="description">
-        /// A description of the expression. If null, <paramref name="expression"/> will be used.
+        /// A description of the expression.
         /// </param>
         /// </param>
         public ExpressionObserver(
         public ExpressionObserver(
             Func<object> rootGetter,
             Func<object> rootGetter,
-            string expression,
+            ExpressionNode node,
             IObservable<Unit> update,
             IObservable<Unit> update,
-            bool enableDataValidation = false,
-            string description = null)
+            string description)
         {
         {
             Contract.Requires<ArgumentNullException>(rootGetter != null);
             Contract.Requires<ArgumentNullException>(rootGetter != null);
-            Contract.Requires<ArgumentNullException>(expression != null);
             Contract.Requires<ArgumentNullException>(update != null);
             Contract.Requires<ArgumentNullException>(update != null);
-
-            Expression = expression;
-            Description = description ?? expression;
-            _node = Parse(expression, enableDataValidation);
+            Description = description;
+            _node = node;
             _node.Target = new WeakReference(rootGetter());
             _node.Target = new WeakReference(rootGetter());
             _root = update.Select(x => rootGetter());
             _root = update.Select(x => rootGetter());
         }
         }
 
 
+
+        /// <summary>
+        /// Creates a new instance of the <see cref="ExpressionObserver"/> class.
+        /// </summary>
+        /// <param name="root">The root object.</param>
+        /// <param name="expression">The expression.</param>
+        /// <param name="enableDataValidation">Whether or not to track data validation</param>
+        /// <param name="description">
+        /// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
+        /// </param>
+        public static ExpressionObserver Create<T, U>(
+            T root,
+            Expression<Func<T, U>> expression,
+            bool enableDataValidation = false,
+            string description = null)
+        {
+            return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString());
+        }
+
+        /// <summary>
+        /// Creates a new instance of the <see cref="ExpressionObserver"/> class.
+        /// </summary>
+        /// <param name="rootObservable">An observable which provides the root object.</param>
+        /// <param name="expression">The expression.</param>
+        /// <param name="enableDataValidation">Whether or not to track data validation</param>
+        /// <param name="description">
+        /// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
+        /// </param>
+        public static ExpressionObserver Create<T, U>(
+            IObservable<T> rootObservable,
+            Expression<Func<T, U>> expression,
+            bool enableDataValidation = false,
+            string description = null)
+        {
+            Contract.Requires<ArgumentNullException>(rootObservable != null);
+            return new ExpressionObserver(
+                rootObservable.Select(o => (object)o),
+                Parse(expression, enableDataValidation),
+                description ?? expression.ToString());
+        }
+
+        /// <summary>
+        /// Creates a new instance of the <see cref="ExpressionObserver"/> class.
+        /// </summary>
+        /// <param name="rootGetter">A function which gets the root object.</param>
+        /// <param name="expression">The expression.</param>
+        /// <param name="update">An observable which triggers a re-read of the getter.</param>
+        /// <param name="enableDataValidation">Whether or not to track data validation</param>
+        /// <param name="description">
+        /// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
+        /// </param>
+        public static ExpressionObserver Create<T, U>(
+            Func<T> rootGetter,
+            Expression<Func<T, U>> expression,
+            IObservable<Unit> update,
+            bool enableDataValidation = false,
+            string description = null)
+        {
+            Contract.Requires<ArgumentNullException>(rootGetter != null);
+
+            return new ExpressionObserver(
+                () => rootGetter(),
+                Parse(expression, enableDataValidation),
+                update,
+                description ?? expression.ToString());
+        }
+
         /// <summary>
         /// <summary>
         /// Attempts to set the value of a property expression.
         /// Attempts to set the value of a property expression.
         /// </summary>
         /// </summary>
@@ -221,16 +276,9 @@ namespace Avalonia.Data.Core
             }
             }
         }
         }
 
 
-        private static ExpressionNode Parse(string expression, bool enableDataValidation)
+        private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation)
         {
         {
-            if (!string.IsNullOrWhiteSpace(expression))
-            {
-                return ExpressionNodeBuilder.Build(expression, enableDataValidation);
-            }
-            else
-            {
-                return new EmptyExpressionNode();
-            }
+            return ExpressionTreeParser.Parse(expression, enableDataValidation);
         }
         }
 
 
         private void StartRoot()
         private void StartRoot()

+ 2 - 2
src/Avalonia.Base/Data/Core/ExpressionParseException.cs

@@ -17,8 +17,8 @@ namespace Avalonia.Data.Core
         /// </summary>
         /// </summary>
         /// <param name="column">The column position of the error.</param>
         /// <param name="column">The column position of the error.</param>
         /// <param name="message">The exception message.</param>
         /// <param name="message">The exception message.</param>
-        public ExpressionParseException(int column, string message)
-            : base(message)
+        public ExpressionParseException(int column, string message, Exception innerException = null)
+            : base(message, innerException)
         {
         {
             Column = column;
             Column = column;
         }
         }

+ 71 - 0
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using Avalonia.Data;
+
+namespace Avalonia.Data.Core
+{
+    class IndexerExpressionNode : IndexerNodeBase
+    {
+        private readonly ParameterExpression _parameter;
+        private readonly IndexExpression _expression;
+        private readonly Delegate _setDelegate;
+        private readonly Delegate _getDelegate;
+        private readonly Delegate _firstArgumentDelegate;
+
+        public IndexerExpressionNode(IndexExpression expression)
+        {
+            _parameter = Expression.Parameter(expression.Object.Type);
+            _expression = expression.Update(_parameter, expression.Arguments);
+
+            _getDelegate = Expression.Lambda(_expression, _parameter).Compile();
+
+            var valueParameter = Expression.Parameter(expression.Type);
+
+            _setDelegate = Expression.Lambda(Expression.Assign(_expression, valueParameter), _parameter, valueParameter).Compile();
+
+            _firstArgumentDelegate = Expression.Lambda(_expression.Arguments[0], _parameter).Compile();
+        }
+
+        public override Type PropertyType => _expression.Type;
+
+        public override string Description => _expression.ToString();
+
+        protected override bool SetTargetValueCore(object value, BindingPriority priority)
+        {
+            try
+            {
+                _setDelegate.DynamicInvoke(Target.Target, value);
+                return true;
+            }
+            catch (Exception)
+            {
+                return false;
+            }
+        }
+
+        protected override object GetValue(object 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 == null || _expression.Indexer.Name == e.PropertyName;
+        }
+
+        protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?;
+    }
+}

+ 92 - 0
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@@ -0,0 +1,92 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reflection;
+using System.Text;
+using Avalonia.Data;
+using Avalonia.Utilities;
+
+namespace Avalonia.Data.Core
+{
+    public abstract class IndexerNodeBase : SettableNode
+    {
+        private IDisposable _subscription;
+        
+        protected override void StartListeningCore(WeakReference reference)
+        {
+            var target = reference.Target;
+            var incc = target as INotifyCollectionChanged;
+            var inpc = target as INotifyPropertyChanged;
+            var inputs = new List<IObservable<object>>();
+
+            if (incc != null)
+            {
+                inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
+                    incc,
+                    nameof(incc.CollectionChanged))
+                    .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
+                    .Select(_ => GetValue(target)));
+            }
+
+            if (inpc != null)
+            {
+                inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
+                    inpc,
+                    nameof(inpc.PropertyChanged))
+                    .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
+                    .Select(_ => GetValue(target)));
+            }
+
+            _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
+        }
+
+        protected override void StopListeningCore()
+        {
+            _subscription.Dispose();
+        }
+
+        protected abstract object GetValue(object target);
+
+        protected abstract int? TryGetFirstArgumentAsInt();
+
+        private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (sender is IList)
+            {
+                var index = TryGetFirstArgumentAsInt();
+
+                if (index == null)
+                {
+                    return false;
+                }
+
+                switch (e.Action)
+                {
+                    case NotifyCollectionChangedAction.Add:
+                        return index >= e.NewStartingIndex;
+                    case NotifyCollectionChangedAction.Remove:
+                        return index >= e.OldStartingIndex;
+                    case NotifyCollectionChangedAction.Replace:
+                        return index >= e.NewStartingIndex &&
+                               index < e.NewStartingIndex + e.NewItems.Count;
+                    case NotifyCollectionChangedAction.Move:
+                        return (index >= e.NewStartingIndex &&
+                                index < e.NewStartingIndex + e.NewItems.Count) ||
+                               (index >= e.OldStartingIndex &&
+                                index < e.OldStartingIndex + e.OldItems.Count);
+                    case NotifyCollectionChangedAction.Reset:
+                        return true;
+                }
+            }
+
+            return true; // Implementation defined meaning for the index, so just try to update anyway
+        }
+
+        protected abstract bool ShouldUpdate(object sender, PropertyChangedEventArgs e);
+    }
+}

+ 1 - 1
src/Avalonia.Base/Data/Core/LogicalNotNode.cs

@@ -7,7 +7,7 @@ using Avalonia.Data;
 
 
 namespace Avalonia.Data.Core
 namespace Avalonia.Data.Core
 {
 {
-    internal class LogicalNotNode : ExpressionNode, ITransformNode
+    public class LogicalNotNode : ExpressionNode, ITransformNode
     {
     {
         public override string Description => "!";
         public override string Description => "!";
 
 

+ 27 - 0
src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+
+namespace Avalonia.Data.Core.Parsers
+{
+    static class ExpressionTreeParser
+    {
+        public static ExpressionNode Parse(Expression expr, bool enableDataValidation)
+        {
+            var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation);
+
+            visitor.Visit(expr);
+
+            var nodes = visitor.Nodes;
+
+            for (int n = 0; n < nodes.Count - 1; ++n)
+            {
+                nodes[n].Next = nodes[n + 1];
+            }
+
+            return nodes.FirstOrDefault() ?? new EmptyExpressionNode();
+        }
+    }
+}

+ 219 - 0
src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs

@@ -0,0 +1,219 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace Avalonia.Data.Core.Parsers
+{
+    class ExpressionVisitorNodeBuilder : ExpressionVisitor
+    {
+        private const string MultiDimensionalArrayGetterMethodName = "Get";
+        private static PropertyInfo AvaloniaObjectIndexer;
+        private static MethodInfo CreateDelegateMethod;
+
+        private readonly bool _enableDataValidation;
+
+        static ExpressionVisitorNodeBuilder()
+        {
+            AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) });
+            CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) });
+        }
+
+        public List<ExpressionNode> Nodes { get; }
+
+        public ExpressionVisitorNodeBuilder(bool enableDataValidation)
+        {
+            _enableDataValidation = enableDataValidation;
+            Nodes = new List<ExpressionNode>();
+        }
+
+        protected override Expression VisitUnary(UnaryExpression node)
+        {
+            if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool))
+            {
+                Nodes.Add(new LogicalNotNode());
+            }
+            else if (node.NodeType == ExpressionType.Convert)
+            {
+                if (node.Operand.Type.IsAssignableFrom(node.Type))
+                {
+                    // Ignore inheritance casts 
+                }
+                else
+                {
+                    throw new ExpressionParseException(0, $"Cannot parse non-inheritance casts in a binding expression.");
+                }
+            }
+            else if (node.NodeType == ExpressionType.TypeAs)
+            {
+                // Ignore as operator.
+            }
+            else
+            {
+                throw new ExpressionParseException(0, $"Unable to parse unary operator {node.NodeType} in a binding expression");
+            }
+
+            return base.VisitUnary(node);
+        }
+
+        protected override Expression VisitMember(MemberExpression node)
+        {
+            var visited = base.VisitMember(node);
+            Nodes.Add(new PropertyAccessorNode(node.Member.Name, _enableDataValidation));
+            return visited;
+        }
+
+        protected override Expression VisitIndex(IndexExpression node)
+        {
+            Visit(node.Object);
+
+            if (node.Indexer == AvaloniaObjectIndexer)
+            {
+                var property = GetArgumentExpressionValue<AvaloniaProperty>(node.Arguments[0]);
+                Nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableDataValidation));
+            }
+            else
+            {
+                Nodes.Add(new IndexerExpressionNode(node));
+            }
+
+            return node;
+        }
+
+        private T GetArgumentExpressionValue<T>(Expression expr)
+        {
+            try
+            {
+                return Expression.Lambda<Func<T>>(expr).Compile(preferInterpretation: true)();
+            }
+            catch (InvalidOperationException ex)
+            {
+                throw new ExpressionParseException(0, "Unable to parse indexer value.", ex);
+            }
+        }
+
+        protected override Expression VisitBinary(BinaryExpression node)
+        {
+            if (node.NodeType == ExpressionType.ArrayIndex)
+            {
+                return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right }));
+            }
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+
+        protected override Expression VisitBlock(BlockExpression node)
+        {
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+
+        protected override CatchBlock VisitCatchBlock(CatchBlock node)
+        {
+            throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions.");
+        }
+
+        protected override Expression VisitConditional(ConditionalExpression node)
+        {
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+
+        protected override Expression VisitDynamic(DynamicExpression node)
+        {
+            throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions.");
+        }
+
+        protected override ElementInit VisitElementInit(ElementInit node)
+        {
+            throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression.");
+        }
+
+        protected override Expression VisitGoto(GotoExpression node)
+        {
+            throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions.");
+        }
+
+        protected override Expression VisitInvocation(InvocationExpression node)
+        {
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+
+        protected override Expression VisitLabel(LabelExpression node)
+        {
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+
+        protected override Expression VisitListInit(ListInitExpression node)
+        {
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+
+        protected override Expression VisitLoop(LoopExpression node)
+        {
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+
+        protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
+        {
+            throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions.");
+        }
+
+        protected override Expression VisitMethodCall(MethodCallExpression node)
+        {
+            if (node.Method == CreateDelegateMethod)
+            {
+                var visited = Visit(node.Arguments[1]);
+                Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue<MethodInfo>(node.Object).Name, _enableDataValidation));
+                return node;
+            }
+            else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`'))
+            {
+                if (node.Method.IsStatic)
+                {
+                    Visit(node.Arguments[0]);
+                }
+                else
+                {
+                    Visit(node.Object);
+                }
+                Nodes.Add(new StreamNode());
+                return node;
+            }
+
+            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)
+        {
+            var type = method.DeclaringType;
+            return type.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method);
+        }
+
+        protected override Expression VisitSwitch(SwitchExpression node)
+        {
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+
+        protected override Expression VisitTry(TryExpression node)
+        {
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+
+        protected override Expression VisitTypeBinary(TypeBinaryExpression node)
+        {
+            throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
+        }
+    }
+}

+ 1 - 29
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@@ -60,35 +60,7 @@ namespace Avalonia.Data.Core.Plugins
 
 
         private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName)
         private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName)
         {
         {
-            if (!propertyName.Contains("."))
-            {
-                return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
-            }
-            else
-            {
-                var split = propertyName.Split('.');
-
-                if (split.Length == 2)
-                {
-                    // HACK: We need a way to resolve types here using something like IXamlTypeResolver.
-                    // We don't currently have that so we have to make our best guess.
-                    var type = split[0];
-                    var name = split[1];
-                    var registry = AvaloniaPropertyRegistry.Instance;
-                    var registered = registry.GetRegisteredAttached(o.GetType())
-                        .Concat(registry.GetRegistered(o.GetType()));
-
-                    foreach (var p in registered)
-                    {
-                        if (p.Name == name && IsOfType(p.OwnerType, type))
-                        {
-                            return p;
-                        }
-                    }
-                }
-            }
-
-            return null;
+            return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
         }
         }
 
 
         private static bool IsOfType(Type type, string typeName)
         private static bool IsOfType(Type type, string typeName)

+ 1 - 1
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@@ -8,7 +8,7 @@ using Avalonia.Data.Core.Plugins;
 
 
 namespace Avalonia.Data.Core
 namespace Avalonia.Data.Core
 {
 {
-    internal class PropertyAccessorNode : SettableNode
+    public class PropertyAccessorNode : SettableNode
     {
     {
         private readonly bool _enableValidation;
         private readonly bool _enableValidation;
         private IPropertyAccessor _accessor;
         private IPropertyAccessor _accessor;

+ 1 - 1
src/Avalonia.Base/Data/Core/SettableNode.cs

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
 
 
 namespace Avalonia.Data.Core
 namespace Avalonia.Data.Core
 {
 {
-    internal abstract class SettableNode : ExpressionNode
+    public abstract class SettableNode : ExpressionNode
     {
     {
         public bool SetTargetValue(object value, BindingPriority priority)
         public bool SetTargetValue(object value, BindingPriority priority)
         {
         {

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

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Avalonia
+{
+    public static class StreamBindingExtensions
+    {
+        internal static string StreamBindingName = "StreamBinding";
+
+        public static T StreamBinding<T>(this Task<T> @this)
+        {
+            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/Avalonia.Base/Data/Core/StreamNode.cs

@@ -6,7 +6,7 @@ using System.Reactive.Linq;
 
 
 namespace Avalonia.Data.Core
 namespace Avalonia.Data.Core
 {
 {
-    internal class StreamNode : ExpressionNode
+    public class StreamNode : ExpressionNode
     {
     {
         private IDisposable _subscription;
         private IDisposable _subscription;
 
 

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            var parser = new SelectorParser((t, ns) => context.ResolveType(ns, t));
+            var parser = new SelectorParser(context.ResolveType);
 
 
             return parser.Parse((string)value);
             return parser.Parse((string)value);
         }
         }

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@@ -37,6 +37,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
 
 
             return new Binding
             return new Binding
             {
             {
+                TypeResolver = descriptorContext.ResolveType,
                 Converter = Converter,
                 Converter = Converter,
                 ConverterParameter = ConverterParameter,
                 ConverterParameter = ConverterParameter,
                 ElementName = pathInfo.ElementName ?? ElementName,
                 ElementName = pathInfo.ElementName ?? ElementName,

+ 2 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs

@@ -4,6 +4,7 @@
 using Avalonia.Controls.Templates;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using System;
 using System;
 using System.Reactive.Linq;
 using System.Reactive.Linq;
 
 
@@ -37,7 +38,7 @@ namespace Avalonia.Markup.Xaml.Templates
                 return o;
                 return o;
             }
             }
 
 
-            var expression = new ExpressionObserver(o, MemberName);
+            var expression = ExpressionObserverBuilder.Build(o, MemberName);
             object result = AvaloniaProperty.UnsetValue;
             object result = AvaloniaProperty.UnsetValue;
 
 
             expression.Subscribe(x => result = x);
             expression.Subscribe(x => result = x);

+ 2 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@@ -8,6 +8,7 @@ using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
 using Avalonia.Markup.Data;
 using Avalonia.Markup.Data;
+using Avalonia.Markup.Parsers;
 using Avalonia.Metadata;
 using Avalonia.Metadata;
 
 
 namespace Avalonia.Markup.Xaml.Templates
 namespace Avalonia.Markup.Xaml.Templates
@@ -41,7 +42,7 @@ namespace Avalonia.Markup.Xaml.Templates
         {
         {
             if (ItemsSource != null)
             if (ItemsSource != null)
             {
             {
-                var obs = new ExpressionObserver(item, ItemsSource.Path);
+                var obs = ExpressionObserverBuilder.Build(item, ItemsSource.Path);
                 return InstancedBinding.OneWay(obs, BindingPriority.Style);
                 return InstancedBinding.OneWay(obs, BindingPriority.Style);
             }
             }
 
 

+ 23 - 12
src/Markup/Avalonia.Markup/Data/Binding.cs

@@ -8,6 +8,7 @@ using System.Reactive.Linq;
 using Avalonia.Data.Converters;
 using Avalonia.Data.Converters;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
 using Avalonia.LogicalTree;
 using Avalonia.LogicalTree;
+using Avalonia.Markup.Parsers;
 using Avalonia.Reactive;
 using Avalonia.Reactive;
 using Avalonia.VisualTree;
 using Avalonia.VisualTree;
 
 
@@ -85,6 +86,11 @@ namespace Avalonia.Data
 
 
         public WeakReference DefaultAnchor { get; set; }
         public WeakReference DefaultAnchor { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets a function used to resolve types from names in the binding path.
+        /// </summary>
+        public Func<string, string, Type> TypeResolver { get; set; }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         public InstancedBinding Initiate(
         public InstancedBinding Initiate(
             IAvaloniaObject target,
             IAvaloniaObject target,
@@ -189,20 +195,22 @@ namespace Avalonia.Data
 
 
             if (!targetIsDataContext)
             if (!targetIsDataContext)
             {
             {
-                var result = new ExpressionObserver(
+                var result = ExpressionObserverBuilder.Build(
                     () => target.GetValue(StyledElement.DataContextProperty),
                     () => target.GetValue(StyledElement.DataContextProperty),
                     path,
                     path,
                     new UpdateSignal(target, StyledElement.DataContextProperty),
                     new UpdateSignal(target, StyledElement.DataContextProperty),
-                    enableDataValidation);
+                    enableDataValidation,
+                    typeResolver: TypeResolver);
 
 
                 return result;
                 return result;
             }
             }
             else
             else
             {
             {
-                return new ExpressionObserver(
+                return ExpressionObserverBuilder.Build(
                     GetParentDataContext(target),
                     GetParentDataContext(target),
                     path,
                     path,
-                    enableDataValidation);
+                    enableDataValidation,
+                    typeResolver: TypeResolver);
             }
             }
         }
         }
 
 
@@ -215,11 +223,12 @@ namespace Avalonia.Data
             Contract.Requires<ArgumentNullException>(target != null);
             Contract.Requires<ArgumentNullException>(target != null);
 
 
             var description = $"#{elementName}.{path}";
             var description = $"#{elementName}.{path}";
-            var result = new ExpressionObserver(
+            var result = ExpressionObserverBuilder.Build(
                 ControlLocator.Track(target, elementName),
                 ControlLocator.Track(target, elementName),
                 path,
                 path,
                 enableDataValidation,
                 enableDataValidation,
-                description);
+                description,
+                typeResolver: TypeResolver);
             return result;
             return result;
         }
         }
 
 
@@ -251,10 +260,11 @@ namespace Avalonia.Data
                     throw new InvalidOperationException("Invalid tree to traverse.");
                     throw new InvalidOperationException("Invalid tree to traverse.");
             }
             }
 
 
-            return new ExpressionObserver(
+            return ExpressionObserverBuilder.Build(
                 controlLocator,
                 controlLocator,
                 path,
                 path,
-                enableDataValidation);
+                enableDataValidation,
+                typeResolver: TypeResolver);
         }
         }
 
 
         private ExpressionObserver CreateSourceObserver(
         private ExpressionObserver CreateSourceObserver(
@@ -264,7 +274,7 @@ namespace Avalonia.Data
         {
         {
             Contract.Requires<ArgumentNullException>(source != null);
             Contract.Requires<ArgumentNullException>(source != null);
 
 
-            return new ExpressionObserver(source, path, enableDataValidation);
+            return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver);
         }
         }
 
 
         private ExpressionObserver CreateTemplatedParentObserver(
         private ExpressionObserver CreateTemplatedParentObserver(
@@ -273,12 +283,13 @@ namespace Avalonia.Data
             bool enableDataValidation)
             bool enableDataValidation)
         {
         {
             Contract.Requires<ArgumentNullException>(target != null);
             Contract.Requires<ArgumentNullException>(target != null);
-
-            var result = new ExpressionObserver(
+            
+            var result = ExpressionObserverBuilder.Build(
                 () => target.GetValue(StyledElement.TemplatedParentProperty),
                 () => target.GetValue(StyledElement.TemplatedParentProperty),
                 path,
                 path,
                 new UpdateSignal(target, StyledElement.TemplatedParentProperty),
                 new UpdateSignal(target, StyledElement.TemplatedParentProperty),
-                enableDataValidation);
+                enableDataValidation,
+                typeResolver: TypeResolver);
 
 
             return result;
             return result;
         }
         }

+ 2 - 1
src/Avalonia.Base/Data/Core/Parsers/ArgumentListParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@@ -1,11 +1,12 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // 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.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 
+using Avalonia.Data.Core;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Text;
 using System.Text;
 
 
-namespace Avalonia.Data.Core.Parsers
+namespace Avalonia.Markup.Parsers
 {
 {
     internal static class ArgumentListParser
     internal static class ArgumentListParser
     {
     {

+ 75 - 0
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs

@@ -0,0 +1,75 @@
+using Avalonia.Data.Core;
+using System;
+using System.Collections.Generic;
+using System.Reactive;
+using System.Text;
+
+namespace Avalonia.Markup.Parsers
+{
+    public static class ExpressionObserverBuilder
+    {
+        internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
+        {
+            if (string.IsNullOrWhiteSpace(expression))
+            {
+                return new EmptyExpressionNode();
+            }
+
+            var reader = new Reader(expression);
+            var parser = new ExpressionParser(enableValidation, typeResolver);
+            var node = parser.Parse(reader);
+
+            if (!reader.End)
+            {
+                throw new ExpressionParseException(reader.Position, "Expected end of expression.");
+            }
+
+            return node;
+        }
+
+        public static ExpressionObserver Build(
+            object root,
+            string expression,
+            bool enableDataValidation = false,
+            string description = null,
+            Func<string, string, Type> typeResolver = null)
+        {
+            return new ExpressionObserver(
+                root,
+                Parse(expression, enableDataValidation, typeResolver),
+                description ?? expression);
+        }
+
+        public static ExpressionObserver Build(
+            IObservable<object> rootObservable,
+            string expression,
+            bool enableDataValidation = false,
+            string description = null,
+            Func<string, string, Type> typeResolver = null)
+        {
+            Contract.Requires<ArgumentNullException>(rootObservable != null);
+            return new ExpressionObserver(
+                rootObservable,
+                Parse(expression, enableDataValidation, typeResolver),
+                description ?? expression);
+        }
+
+
+        public static ExpressionObserver Build(
+            Func<object> rootGetter,
+            string expression,
+            IObservable<Unit> update,
+            bool enableDataValidation = false,
+            string description = null,
+            Func<string, string, Type> typeResolver = null)
+        {
+            Contract.Requires<ArgumentNullException>(rootGetter != null);
+
+            return new ExpressionObserver(
+                () => rootGetter(),
+                Parse(expression, enableDataValidation, typeResolver),
+                update,
+                description ?? expression);
+        }
+    }
+}

+ 29 - 6
src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@@ -1,18 +1,22 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // 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.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 
+using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers.Nodes;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 
 
-namespace Avalonia.Data.Core.Parsers
+namespace Avalonia.Markup.Parsers
 {
 {
     internal class ExpressionParser
     internal class ExpressionParser
     {
     {
-        private bool _enableValidation;
+        private readonly bool _enableValidation;
+        private readonly Func<string, string, Type> _typeResolver;
 
 
-        public ExpressionParser(bool enableValidation)
+        public ExpressionParser(bool enableValidation, Func<string, string, Type> typeResolver)
         {
         {
+            _typeResolver = typeResolver;
             _enableValidation = enableValidation;
             _enableValidation = enableValidation;
         }
         }
 
 
@@ -130,7 +134,19 @@ namespace Avalonia.Data.Core.Parsers
 
 
         private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
         private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
         {
         {
-            var owner = IdentifierParser.Parse(r);
+            string ns = string.Empty;
+            string owner;
+            var ownerOrNamespace = IdentifierParser.Parse(r);
+
+            if (r.TakeIf(':'))
+            {
+                ns = ownerOrNamespace;
+                owner = IdentifierParser.Parse(r);
+            }
+            else
+            {
+                owner = ownerOrNamespace;
+            }
 
 
             if (r.End || !r.TakeIf('.'))
             if (r.End || !r.TakeIf('.'))
             {
             {
@@ -144,7 +160,14 @@ namespace Avalonia.Data.Core.Parsers
                 throw new ExpressionParseException(r.Position, "Expected ')'.");
                 throw new ExpressionParseException(r.Position, "Expected ')'.");
             }
             }
 
 
-            nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation));
+            if (_typeResolver == null)
+            {
+                throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?");
+            }
+
+            var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns, owner), name);
+
+            nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation));
             return State.AfterMember;
             return State.AfterMember;
         }
         }
 
 
@@ -157,7 +180,7 @@ namespace Avalonia.Data.Core.Parsers
                 throw new ExpressionParseException(r.Position, "Indexer may not be empty.");
                 throw new ExpressionParseException(r.Position, "Indexer may not be empty.");
             }
             }
 
 
-            nodes.Add(new IndexerNode(args));
+            nodes.Add(new StringIndexerNode(args));
             return State.AfterMember;
             return State.AfterMember;
         }
         }
         
         

+ 1 - 1
src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs

@@ -4,7 +4,7 @@
 using System.Globalization;
 using System.Globalization;
 using System.Text;
 using System.Text;
 
 
-namespace Avalonia.Data.Core.Parsers
+namespace Avalonia.Markup.Parsers
 {
 {
     internal static class IdentifierParser
     internal static class IdentifierParser
     {
     {

+ 14 - 74
src/Avalonia.Base/Data/Core/IndexerNode.cs → src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs

@@ -12,53 +12,19 @@ using System.Linq;
 using System.Reflection;
 using System.Reflection;
 using System.Reactive.Linq;
 using System.Reactive.Linq;
 using Avalonia.Data;
 using Avalonia.Data;
+using Avalonia.Data.Core;
 
 
-namespace Avalonia.Data.Core
+namespace Avalonia.Markup.Parsers.Nodes
 {
 {
-    internal class IndexerNode :  SettableNode
+    internal class StringIndexerNode : IndexerNodeBase
     {
     {
-        private IDisposable _subscription;
-
-        public IndexerNode(IList<string> arguments)
+        public StringIndexerNode(IList<string> arguments)
         {
         {
             Arguments = arguments;
             Arguments = arguments;
         }
         }
 
 
         public override string Description => "[" + string.Join(",", Arguments) + "]";
         public override string Description => "[" + string.Join(",", Arguments) + "]";
 
 
-        protected override void StartListeningCore(WeakReference reference)
-        {
-            var target = reference.Target;
-            var incc = target as INotifyCollectionChanged;
-            var inpc = target as INotifyPropertyChanged;
-            var inputs = new List<IObservable<object>>();
-
-            if (incc != null)
-            {
-                inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
-                    incc,
-                    nameof(incc.CollectionChanged))
-                    .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
-                    .Select(_ => GetValue(target)));
-            }
-
-            if (inpc != null)
-            {
-                inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
-                    inpc,
-                    nameof(inpc.PropertyChanged))
-                    .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
-                    .Select(_ => GetValue(target)));
-            }
-
-            _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
-        }
-
-        protected override void StopListeningCore()
-        {
-            _subscription.Dispose();
-        }
-
         protected override bool SetTargetValueCore(object value, BindingPriority priority)
         protected override bool SetTargetValueCore(object value, BindingPriority priority)
         {
         {
             var typeInfo = Target.Target.GetType().GetTypeInfo();
             var typeInfo = Target.Target.GetType().GetTypeInfo();
@@ -163,7 +129,7 @@ namespace Avalonia.Data.Core
 
 
         public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType;
         public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType;
 
 
-        private object GetValue(object target)
+        protected override object GetValue(object target)
         {
         {
             var typeInfo = target.GetType().GetTypeInfo();
             var typeInfo = target.GetType().GetTypeInfo();
             var list = target as IList;
             var list = target as IList;
@@ -316,45 +282,19 @@ namespace Avalonia.Data.Core
             }
             }
         }
         }
 
 
-        private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e)
+        protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
         {
         {
-            if (sender is IList)
-            {
-                object indexObject;
-
-                if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject))
-                {
-                    return false;
-                }
-
-                var index = (int)indexObject;
-
-                switch (e.Action)
-                {
-                    case NotifyCollectionChangedAction.Add:
-                        return index >= e.NewStartingIndex;
-                    case NotifyCollectionChangedAction.Remove:
-                        return index >= e.OldStartingIndex;
-                    case NotifyCollectionChangedAction.Replace:
-                        return index >= e.NewStartingIndex &&
-                               index < e.NewStartingIndex + e.NewItems.Count;
-                    case NotifyCollectionChangedAction.Move:
-                        return (index >= e.NewStartingIndex &&
-                                index < e.NewStartingIndex + e.NewItems.Count) ||
-                               (index >= e.OldStartingIndex &&
-                                index < e.OldStartingIndex + e.OldItems.Count);
-                    case NotifyCollectionChangedAction.Reset:
-                        return true;
-                }
-            }
-
-            return true; // Implementation defined meaning for the index, so just try to update anyway
+            var typeInfo = sender.GetType().GetTypeInfo();
+            return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
         }
         }
 
 
-        private bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
+        protected override int? TryGetFirstArgumentAsInt()
         {
         {
-            var typeInfo = sender.GetType().GetTypeInfo();
-            return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
+            if (TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out var value))
+            {
+                return (int?)value;
+            }
+            return null;
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/Avalonia.Base/Data/Core/Parsers/Reader.cs → src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs

@@ -3,7 +3,7 @@
 
 
 using System;
 using System;
 
 
-namespace Avalonia.Data.Core.Parsers
+namespace Avalonia.Markup.Parsers
 {
 {
     internal class Reader
     internal class Reader
     {
     {

+ 2 - 2
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@@ -52,11 +52,11 @@ namespace Avalonia.Markup.Parsers
 
 
                 if (ofType != null)
                 if (ofType != null)
                 {
                 {
-                    result = result.OfType(_typeResolver(ofType.TypeName, ofType.Xmlns));
+                    result = result.OfType(_typeResolver(ofType.Xmlns, ofType.TypeName));
                 }
                 }
                 if (@is != null)
                 if (@is != null)
                 {
                 {
-                    result = result.Is(_typeResolver(@is.TypeName, @is.Xmlns));
+                    result = result.Is(_typeResolver(@is.Xmlns, @is.TypeName));
                 }
                 }
                 else if (@class != null)
                 else if (@class != null)
                 {
                 {

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

@@ -10,6 +10,7 @@ using System.Threading.Tasks;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Data.Converters;
 using Avalonia.Data.Converters;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
 using Moq;
 using Moq;
 using Xunit;
 using Xunit;
@@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_Property_Value()
         public async Task Should_Get_Simple_Property_Value()
         {
         {
             var data = new Class1 { StringValue = "foo" };
             var data = new Class1 { StringValue = "foo" };
-            var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("foo", result);
             Assert.Equal("foo", result);
@@ -34,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Set_Simple_Property_Value()
         public void Should_Set_Simple_Property_Value()
         {
         {
             var data = new Class1 { StringValue = "foo" };
             var data = new Class1 { StringValue = "foo" };
-            var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
 
 
             target.OnNext("bar");
             target.OnNext("bar");
 
 
@@ -47,7 +48,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Set_Indexed_Value()
         public void Should_Set_Indexed_Value()
         {
         {
             var data = new { Foo = new[] { "foo" } };
             var data = new { Foo = new[] { "foo" } };
-            var target = new BindingExpression(new ExpressionObserver(data, "Foo[0]"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.Foo[0]), typeof(string));
 
 
             target.OnNext("bar");
             target.OnNext("bar");
 
 
@@ -60,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Convert_Get_String_To_Double()
         public async Task Should_Convert_Get_String_To_Double()
         {
         {
             var data = new Class1 { StringValue = $"{5.6}" };
             var data = new Class1 { StringValue = $"{5.6}" };
-            var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal(5.6, result);
             Assert.Equal(5.6, result);
@@ -72,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Getting_Invalid_Double_String_Should_Return_BindingError()
         public async Task Getting_Invalid_Double_String_Should_Return_BindingError()
         {
         {
             var data = new Class1 { StringValue = "foo" };
             var data = new Class1 { StringValue = "foo" };
-            var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.IsType<BindingNotification>(result);
             Assert.IsType<BindingNotification>(result);
@@ -84,7 +85,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue()
         public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue()
         {
         {
             var data = new Class1 { StringValue = null };
             var data = new Class1 { StringValue = null };
-            var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
@@ -96,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Convert_Set_String_To_Double()
         public void Should_Convert_Set_String_To_Double()
         {
         {
             var data = new Class1 { StringValue = $"{5.6}" };
             var data = new Class1 { StringValue = $"{5.6}" };
-            var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
 
 
             target.OnNext(6.7);
             target.OnNext(6.7);
 
 
@@ -109,7 +110,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Convert_Get_Double_To_String()
         public async Task Should_Convert_Get_Double_To_String()
         {
         {
             var data = new Class1 { DoubleValue = 5.6 };
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal($"{5.6}", result);
             Assert.Equal($"{5.6}", result);
@@ -121,7 +122,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Convert_Set_Double_To_String()
         public void Should_Convert_Set_Double_To_String()
         {
         {
             var data = new Class1 { DoubleValue = 5.6 };
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
 
 
             target.OnNext($"{6.7}");
             target.OnNext($"{6.7}");
 
 
@@ -135,7 +136,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             var data = new Class1 { StringValue = "foo" };
             var data = new Class1 { StringValue = "foo" };
             var target = new BindingExpression(
             var target = new BindingExpression(
-                new ExpressionObserver(data, "StringValue"),
+                ExpressionObserver.Create(data, o => o.StringValue),
                 typeof(int),
                 typeof(int),
                 42,
                 42,
                 DefaultValueConverter.Instance);
                 DefaultValueConverter.Instance);
@@ -156,7 +157,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             var data = new Class1 { StringValue = "foo" };
             var data = new Class1 { StringValue = "foo" };
             var target = new BindingExpression(
             var target = new BindingExpression(
-                new ExpressionObserver(data, "StringValue", true),
+                ExpressionObserver.Create(data, o => o.StringValue, true),
                 typeof(int),
                 typeof(int),
                 42,
                 42,
                 DefaultValueConverter.Instance);
                 DefaultValueConverter.Instance);
@@ -177,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             var data = new Class1 { StringValue = "foo" };
             var data = new Class1 { StringValue = "foo" };
             var target = new BindingExpression(
             var target = new BindingExpression(
-                new ExpressionObserver(data, "StringValue"),
+                ExpressionObserver.Create(data, o => o.StringValue),
                 typeof(int),
                 typeof(int),
                 "bar",
                 "bar",
                 DefaultValueConverter.Instance);
                 DefaultValueConverter.Instance);
@@ -199,7 +200,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             var data = new Class1 { StringValue = "foo" };
             var data = new Class1 { StringValue = "foo" };
             var target = new BindingExpression(
             var target = new BindingExpression(
-                new ExpressionObserver(data, "StringValue", true),
+                ExpressionObserver.Create(data, o => o.StringValue, true),
                 typeof(int),
                 typeof(int),
                 "bar",
                 "bar",
                 DefaultValueConverter.Instance);
                 DefaultValueConverter.Instance);
@@ -220,7 +221,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Setting_Invalid_Double_String_Should_Not_Change_Target()
         public void Setting_Invalid_Double_String_Should_Not_Change_Target()
         {
         {
             var data = new Class1 { DoubleValue = 5.6 };
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
 
 
             target.OnNext("foo");
             target.OnNext("foo");
 
 
@@ -234,7 +235,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             var data = new Class1 { DoubleValue = 5.6 };
             var data = new Class1 { DoubleValue = 5.6 };
             var target = new BindingExpression(
             var target = new BindingExpression(
-                new ExpressionObserver(data, "DoubleValue"),
+                ExpressionObserver.Create(data, o => o.DoubleValue),
                 typeof(string),
                 typeof(string),
                 "9.8",
                 "9.8",
                 DefaultValueConverter.Instance);
                 DefaultValueConverter.Instance);
@@ -250,7 +251,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Coerce_Setting_Null_Double_To_Default_Value()
         public void Should_Coerce_Setting_Null_Double_To_Default_Value()
         {
         {
             var data = new Class1 { DoubleValue = 5.6 };
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
 
 
             target.OnNext(null);
             target.OnNext(null);
 
 
@@ -263,7 +264,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
         public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
         {
         {
             var data = new Class1 { DoubleValue = 5.6 };
             var data = new Class1 { DoubleValue = 5.6 };
-            var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
 
 
             target.OnNext(AvaloniaProperty.UnsetValue);
             target.OnNext(AvaloniaProperty.UnsetValue);
 
 
@@ -279,7 +280,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var converter = new Mock<IValueConverter>();
             var converter = new Mock<IValueConverter>();
 
 
             var target = new BindingExpression(
             var target = new BindingExpression(
-                new ExpressionObserver(data, "DoubleValue"),
+                ExpressionObserver.Create(data, o => o.DoubleValue),
                 typeof(string),
                 typeof(string),
                 converter.Object,
                 converter.Object,
                 converterParameter: "foo");
                 converterParameter: "foo");
@@ -297,7 +298,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var data = new Class1 { DoubleValue = 5.6 };
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
             var converter = new Mock<IValueConverter>();
             var target = new BindingExpression(
             var target = new BindingExpression(
-                new ExpressionObserver(data, "DoubleValue"),
+                ExpressionObserver.Create(data, o => o.DoubleValue),
                 typeof(string),
                 typeof(string),
                 converter.Object,
                 converter.Object,
                 converterParameter: "foo");
                 converterParameter: "foo");
@@ -314,7 +315,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             var data = new Class1 { DoubleValue = 5.6 };
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
             var converter = new Mock<IValueConverter>();
-            var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue, true), typeof(string));
             var result = new List<object>();
             var result = new List<object>();
 
 
             target.Subscribe(x => result.Add(x));
             target.Subscribe(x => result.Add(x));
@@ -341,7 +342,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Second_Subscription_Should_Fire_Immediately()
         public void Second_Subscription_Should_Fire_Immediately()
         {
         {
             var data = new Class1 { StringValue = "foo" };
             var data = new Class1 { StringValue = "foo" };
-            var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
+            var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string));
             object result = null;
             object result = null;
 
 
             target.Subscribe();
             target.Subscribe();

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

@@ -13,16 +13,12 @@ namespace Avalonia.Base.UnitTests.Data.Core
 {
 {
     public class ExpressionObserverTests_AttachedProperty
     public class ExpressionObserverTests_AttachedProperty
     {
     {
-        public ExpressionObserverTests_AttachedProperty()
-        {
-            var foo = Owner.FooProperty;
-        }
 
 
         [Fact]
         [Fact]
         public async Task Should_Get_Attached_Property_Value()
         public async Task Should_Get_Attached_Property_Value()
         {
         {
             var data = new Class1();
             var data = new Class1();
-            var target = new ExpressionObserver(data, "(Owner.Foo)");
+            var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("foo", result);
             Assert.Equal("foo", result);
@@ -41,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 }
                 }
             };
             };
 
 
-            var target = new ExpressionObserver(data, "Next.(Owner.Foo)");
+            var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("bar", result);
             Assert.Equal("bar", result);
@@ -53,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_Simple_Attached_Value()
         public void Should_Track_Simple_Attached_Value()
         {
         {
             var data = new Class1();
             var data = new Class1();
-            var target = new ExpressionObserver(data, "(Owner.Foo)");
+            var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -77,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 }
                 }
             };
             };
 
 
-            var target = new ExpressionObserver(data, "Next.(Owner.Foo)");
+            var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -96,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
             Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
             {
             {
                 var source = new Class1();
                 var source = new Class1();
-                var target = new ExpressionObserver(source, "(Owner.Foo)");
+                var target = ExpressionObserver.Create(source, o => o.Next[Owner.FooProperty]);
                 return Tuple.Create(target, new WeakReference(source));
                 return Tuple.Create(target, new WeakReference(source));
             };
             };
 
 
@@ -108,22 +104,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Assert.Null(result.Item2.Target);
             Assert.Null(result.Item2.Target);
         }
         }
 
 
-        [Fact]
-        public void Should_Fail_With_Attached_Property_With_Only_1_Part()
-        {
-            var data = new Class1();
-
-            Assert.Throws<ExpressionParseException>(() => new ExpressionObserver(data, "(Owner)"));
-        }
-
-        [Fact]
-        public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts()
-        {
-            var data = new Class1();
-
-            Assert.Throws<ExpressionParseException>(() => new ExpressionObserver(data, "(Owner.Foo.Bar)"));
-        }
-
         private static class Owner
         private static class Owner
         {
         {
             public static readonly AttachedProperty<string> FooProperty =
             public static readonly AttachedProperty<string> FooProperty =

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

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

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

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

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

@@ -0,0 +1,224 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Data.Core;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Data.Core
+{
+    public class ExpressionObserverTests_ExpressionTree
+    {
+        [Fact]
+        public async Task IdentityExpression_Creates_IdentityObserver()
+        {
+            var target = new object();
+
+            var observer = ExpressionObserver.Create(target, o => o);
+
+            Assert.Equal(target, await observer.Take(1));
+            GC.KeepAlive(target);
+        }
+
+        [Fact]
+        public async Task Property_Access_Expression_Observes_Property()
+        {
+            var target = new Class1();
+
+            var observer = ExpressionObserver.Create(target, o => o.Foo);
+
+            Assert.Null(await observer.Take(1));
+
+            using (observer.Subscribe(_ => {}))
+            {
+                target.Foo = "Test"; 
+            }
+
+            Assert.Equal("Test", await observer.Take(1));
+
+            GC.KeepAlive(target);
+        }
+
+        [Fact]
+        public void Property_Acccess_Expression_Can_Set_Property()
+        {
+            var data = new Class1();
+            var target = ExpressionObserver.Create(data, o => o.Foo);
+
+            using (target.Subscribe(_ => { }))
+            {
+                Assert.True(target.SetValue("baz"));
+            }
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Indexer_Accessor_Can_Read_Value()
+        {
+            var data = new[] { 1, 2, 3, 4 };
+
+            var target = ExpressionObserver.Create(data, o => o[0]);
+
+            Assert.Equal(data[0], await target.Take(1));
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Indexer_List_Accessor_Can_Read_Value()
+        {
+            var data = new List<int> { 1, 2, 3, 4 };
+
+            var target = ExpressionObserver.Create(data, o => o[0]);
+
+            Assert.Equal(data[0], await target.Take(1));
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Indexer_Accessor_Can_Read_Complex_Index()
+        {
+            var data = new Dictionary<object, object>();
+
+            var key = new object();
+
+            data.Add(key, new object());
+
+            var target = ExpressionObserver.Create(data, o => o[key]);
+
+            Assert.Equal(data[key], await target.Take(1));
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public void Indexer_Can_Set_Value()
+        {
+            var data = new[] { 1, 2, 3, 4 };
+
+            var target = ExpressionObserver.Create(data, o => o[0]);
+
+            using (target.Subscribe(_ => { }))
+            {
+                Assert.True(target.SetValue(2));
+            }
+
+            GC.KeepAlive(data);
+        }
+
+        [Fact]
+        public async Task Inheritance_Casts_Should_Be_Ignored()
+        {
+            NotifyingBase test = new Class1 { Foo = "Test" };
+
+            var target = ExpressionObserver.Create(test, o => ((Class1)o).Foo);
+
+            Assert.Equal("Test", await target.Take(1));
+
+            GC.KeepAlive(test);
+        }
+
+        [Fact]
+        public void Convert_Casts_Should_Error()
+        {
+            var test = 1;
+
+            Assert.Throws<ExpressionParseException>(() => ExpressionObserver.Create(test, o => (double)o));
+        }
+
+        [Fact]
+        public async Task As_Operator_Should_Be_Ignored()
+        {
+            NotifyingBase test = new Class1 { Foo = "Test" };
+
+            var target = ExpressionObserver.Create(test, o => (o as Class1).Foo);
+
+            Assert.Equal("Test", await target.Take(1));
+
+            GC.KeepAlive(test);
+        }
+
+        [Fact]
+        public async Task Avalonia_Property_Indexer_Reads_Avalonia_Property_Value()
+        {
+            var test = new Class2();
+
+            var target = ExpressionObserver.Create(test, o => o[Class2.FooProperty]);
+
+            Assert.Equal("foo", await target.Take(1));
+
+            GC.KeepAlive(test);
+        }
+
+        [Fact]
+        public async Task Complex_Expression_Correctly_Parsed()
+        {
+            var test = new Class1 { Foo = "Test" };
+
+            var target = ExpressionObserver.Create(test, o => o.Foo.Length);
+
+            Assert.Equal(test.Foo.Length, await target.Take(1));
+
+            GC.KeepAlive(test);
+        }
+
+        [Fact]
+        public void Should_Get_Completed_Task_Value()
+        {
+            using (var sync = UnitTestSynchronizationContext.Begin())
+            {
+                var data = new { Foo = Task.FromResult("foo") };
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
+                var result = new List<object>();
+
+                var sub = target.Subscribe(x => result.Add(x));
+
+                Assert.Equal(new[] { "foo" }, result);
+
+                GC.KeepAlive(data);
+            }
+        }
+
+        [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;
+
+            public string Foo
+            {
+                get { return _foo; }
+                set
+                {
+                    _foo = value;
+                    RaisePropertyChanged(nameof(Foo));
+                }
+            }
+        }
+
+
+        private class Class2 : AvaloniaObject
+        {
+            public static readonly StyledProperty<string> FooProperty =
+                AvaloniaProperty.Register<Class2, string>("Foo", defaultValue: "foo");
+
+            public string ClrProperty { get; } = "clr-property";
+        }
+
+        private class Class3
+        {
+            public void Method() { }
+        }
+    }
+}

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

@@ -11,6 +11,7 @@ using Avalonia.Diagnostics;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
 using Xunit;
 using Xunit;
+using Avalonia.Markup.Parsers;
 
 
 namespace Avalonia.Base.UnitTests.Data.Core
 namespace Avalonia.Base.UnitTests.Data.Core
 {
 {
@@ -20,7 +21,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Array_Value()
         public async Task Should_Get_Array_Value()
         {
         {
             var data = new { Foo = new [] { "foo", "bar" } };
             var data = new { Foo = new [] { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, x => x.Foo[1]);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("bar", result);
             Assert.Equal("bar", result);
@@ -28,47 +29,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
             GC.KeepAlive(data);
         }
         }
 
 
-        [Fact]
-        public async Task Should_Get_UnsetValue_For_Invalid_Array_Index()
-        {
-            var data = new { Foo = new[] { "foo", "bar" } };
-            var target = new ExpressionObserver(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 = new ExpressionObserver(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 = new ExpressionObserver(data, "Foo[noindexer]");
-            var result = await target.Take(1);
-
-            Assert.Equal(AvaloniaProperty.UnsetValue, result);
-
-            GC.KeepAlive(data);
-        }
-
         [Fact]
         [Fact]
         public async Task Should_Get_MultiDimensional_Array_Value()
         public async Task Should_Get_MultiDimensional_Array_Value()
         {
         {
             var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
             var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } };
-            var target = new ExpressionObserver(data, "Foo[1, 1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1, 1]);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("qux", result);
             Assert.Equal("qux", result);
@@ -80,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Value_For_String_Indexer()
         public async Task Should_Get_Value_For_String_Indexer()
         {
         {
             var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
             var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
-            var target = new ExpressionObserver(data, "Foo[foo]");
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("bar", result);
             Assert.Equal("bar", result);
@@ -92,7 +57,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Value_For_Non_String_Indexer()
         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 data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
-            var target = new ExpressionObserver(data, "Foo[1.0]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1.0]);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("bar", result);
             Assert.Equal("bar", result);
@@ -104,19 +69,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
         public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
         {
         {
             var data = new { Foo = new[] { "foo", "bar" } };
             var data = new { Foo = new[] { "foo", "bar" } };
-            var target = new ExpressionObserver(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 = new ExpressionObserver(data, "Foo[1,2]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[2]);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
@@ -128,7 +81,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
         public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
         {
         {
             var data = new { Foo = new List<string> { "foo", "bar" } };
             var data = new { Foo = new List<string> { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "Foo[2]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[2]);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
             Assert.Equal(AvaloniaProperty.UnsetValue, result);
@@ -140,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_List_Value()
         public async Task Should_Get_List_Value()
         {
         {
             var data = new { Foo = new List<string> { "foo", "bar" } };
             var data = new { Foo = new List<string> { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("bar", result);
             Assert.Equal("bar", result);
@@ -152,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Add()
         public void Should_Track_INCC_Add()
         {
         {
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "Foo[2]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[2]);
             var result = new List<object>();
             var result = new List<object>();
 
 
             using (var sub = target.Subscribe(x => result.Add(x)))
             using (var sub = target.Subscribe(x => result.Add(x)))
@@ -170,7 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Remove()
         public void Should_Track_INCC_Remove()
         {
         {
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "Foo[0]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[0]);
             var result = new List<object>();
             var result = new List<object>();
 
 
             using (var sub = target.Subscribe(x => result.Add(x)))
             using (var sub = target.Subscribe(x => result.Add(x)))
@@ -188,7 +141,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Replace()
         public void Should_Track_INCC_Replace()
         {
         {
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
             var result = new List<object>();
             var result = new List<object>();
 
 
             using (var sub = target.Subscribe(x => result.Add(x)))
             using (var sub = target.Subscribe(x => result.Add(x)))
@@ -209,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             // method, but even if it did we need to test with ObservableCollection as well
             // 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.
             // as AvaloniaList as it implements PropertyChanged as an explicit interface event.
             var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
             var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -225,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_INCC_Reset()
         public void Should_Track_INCC_Reset()
         {
         {
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
             var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -244,7 +197,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             data.Foo["foo"] = "bar";
             data.Foo["foo"] = "bar";
             data.Foo["baz"] = "qux";
             data.Foo["baz"] = "qux";
 
 
-            var target = new ExpressionObserver(data, "Foo[foo]");
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
             var result = new List<object>();
             var result = new List<object>();
 
 
             using (var sub = target.Subscribe(x => result.Add(x)))
             using (var sub = target.Subscribe(x => result.Add(x)))
@@ -263,7 +216,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_SetArrayIndex()
         public void Should_SetArrayIndex()
         {
         {
             var data = new { Foo = new[] { "foo", "bar" } };
             var data = new { Foo = new[] { "foo", "bar" } };
-            var target = new ExpressionObserver(data, "Foo[1]");
+            var target = ExpressionObserver.Create(data, o => o.Foo[1]);
 
 
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
@@ -285,8 +238,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     {"foo", 1 }
                     {"foo", 1 }
                 }
                 }
             };
             };
-            
-            var target = new ExpressionObserver(data, "Foo[foo]");
+
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
                 Assert.True(target.SetValue(4));
                 Assert.True(target.SetValue(4));
@@ -307,8 +260,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     {"foo", 1 }
                     {"foo", 1 }
                 }
                 }
             };
             };
-            
-            var target = new ExpressionObserver(data, "Foo[bar]");
+
+            var target = ExpressionObserver.Create(data, o => o.Foo["bar"]);
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
                 Assert.True(target.SetValue(4));
                 Assert.True(target.SetValue(4));
@@ -326,7 +279,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             data.Foo["foo"] = "bar";
             data.Foo["foo"] = "bar";
             data.Foo["baz"] = "qux";
             data.Foo["baz"] = "qux";
 
 
-            var target = new ExpressionObserver(data, "Foo[foo]");
+            var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
 
 
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
@@ -343,7 +296,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             var data = new[] { 1, 2, 3 };
             var data = new[] { 1, 2, 3 };
 
 
-            var target = new ExpressionObserver(data, "[1]");
+            var target = ExpressionObserver.Create(data, o => o[1]);
 
 
             var value = await target.Take(1);
             var value = await target.Take(1);
 
 

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

@@ -9,6 +9,7 @@ using System.Reactive.Subjects;
 using Microsoft.Reactive.Testing;
 using Microsoft.Reactive.Testing;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
 using Xunit;
 using Xunit;
+using Avalonia.Markup.Parsers;
 
 
 namespace Avalonia.Base.UnitTests.Data.Core
 namespace Avalonia.Base.UnitTests.Data.Core
 {
 {
@@ -18,7 +19,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Complete_When_Source_Observable_Completes()
         public void Should_Complete_When_Source_Observable_Completes()
         {
         {
             var source = new BehaviorSubject<object>(1);
             var source = new BehaviorSubject<object>(1);
-            var target = new ExpressionObserver(source, "Foo");
+            var target = ExpressionObserver.Create<object, object>(source, o => o);
             var completed = false;
             var completed = false;
 
 
             target.Subscribe(_ => { }, () => completed = true);
             target.Subscribe(_ => { }, () => completed = true);
@@ -31,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Complete_When_Source_Observable_Errors()
         public void Should_Complete_When_Source_Observable_Errors()
         {
         {
             var source = new BehaviorSubject<object>(1);
             var source = new BehaviorSubject<object>(1);
-            var target = new ExpressionObserver(source, "Foo");
+            var target = ExpressionObserver.Create<object, object>(source, o => o);
             var completed = false;
             var completed = false;
 
 
             target.Subscribe(_ => { }, () => completed = true);
             target.Subscribe(_ => { }, () => completed = true);
@@ -44,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Complete_When_Update_Observable_Completes()
         public void Should_Complete_When_Update_Observable_Completes()
         {
         {
             var update = new Subject<Unit>();
             var update = new Subject<Unit>();
-            var target = new ExpressionObserver(() => 1, "Foo", update);
+            var target = ExpressionObserver.Create(() => 1, o => o, update);
             var completed = false;
             var completed = false;
 
 
             target.Subscribe(_ => { }, () => completed = true);
             target.Subscribe(_ => { }, () => completed = true);
@@ -57,7 +58,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Complete_When_Update_Observable_Errors()
         public void Should_Complete_When_Update_Observable_Errors()
         {
         {
             var update = new Subject<Unit>();
             var update = new Subject<Unit>();
-            var target = new ExpressionObserver(() => 1, "Foo", update);
+            var target = ExpressionObserver.Create(() => 1, o => o, update);
             var completed = false;
             var completed = false;
 
 
             target.Subscribe(_ => { }, () => completed = true);
             target.Subscribe(_ => { }, () => completed = true);
@@ -72,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var scheduler = new TestScheduler();
             var scheduler = new TestScheduler();
             var source = scheduler.CreateColdObservable(
             var source = scheduler.CreateColdObservable(
                 OnNext(1, new { Foo = "foo" }));
                 OnNext(1, new { Foo = "foo" }));
-            var target = new ExpressionObserver(source, "Foo");
+            var target = ExpressionObserver.Create(source, o => o.Foo);
             var result = new List<object>();
             var result = new List<object>();
 
 
             using (target.Subscribe(x => result.Add(x)))
             using (target.Subscribe(x => result.Add(x)))
@@ -91,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var scheduler = new TestScheduler();
             var scheduler = new TestScheduler();
             var update = scheduler.CreateColdObservable<Unit>();
             var update = scheduler.CreateColdObservable<Unit>();
             var data = new { Foo = "foo" };
             var data = new { Foo = "foo" };
-            var target = new ExpressionObserver(() => data, "Foo", update);
+            var target = ExpressionObserver.Create(() => data, o => o.Foo, update);
             var result = new List<object>();
             var result = new List<object>();
 
 
             using (target.Subscribe(x => result.Add(x)))
             using (target.Subscribe(x => result.Add(x)))
@@ -106,9 +107,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
             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));
         }
         }
     }
     }
 }
 }

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

@@ -6,6 +6,7 @@ using System.Reactive.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using Xunit;
 using Xunit;
 
 
 namespace Avalonia.Base.UnitTests.Data.Core
 namespace Avalonia.Base.UnitTests.Data.Core
@@ -16,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Negate_Boolean_Value()
         public async Task Should_Negate_Boolean_Value()
         {
         {
             var data = new { Foo = true };
             var data = new { Foo = true };
-            var target = new ExpressionObserver(data, "!Foo");
+            var target = ExpressionObserver.Create(data, o => !o.Foo);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.False((bool)result);
             Assert.False((bool)result);
@@ -24,103 +25,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
             GC.KeepAlive(data);
         }
         }
 
 
-        [Fact]
-        public async Task Should_Negate_0()
-        {
-            var data = new { Foo = 0 };
-            var target = new ExpressionObserver(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 = new ExpressionObserver(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 = new ExpressionObserver(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 = new ExpressionObserver(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 = new ExpressionObserver(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 = new ExpressionObserver(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 = new ExpressionObserver(data, "!Foo");
-            target.Subscribe(_ => { });
-
-            Assert.False(target.SetValue("bar"));
-
-            GC.KeepAlive(data);
-        }
-
         [Fact]
         [Fact]
         public void Can_SetValue_For_Valid_Value()
         public void Can_SetValue_For_Valid_Value()
         {
         {
             var data = new Test { Foo = true };
             var data = new Test { Foo = true };
-            var target = new ExpressionObserver(data, "!Foo");
+            var target = ExpressionObserver.Create(data, o => !o.Foo);
             target.Subscribe(_ => { });
             target.Subscribe(_ => { });
 
 
             Assert.True(target.SetValue(true));
             Assert.True(target.SetValue(true));

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

@@ -7,6 +7,7 @@ using System.Reactive.Linq;
 using System.Reactive.Subjects;
 using System.Reactive.Subjects;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
 using Xunit;
 using Xunit;
 
 
@@ -15,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
     public class ExpressionObserverTests_Observable
     public class ExpressionObserverTests_Observable
     {
     {
         [Fact]
         [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())
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
             {
                 var source = new BehaviorSubject<string>("foo");
                 var source = new BehaviorSubject<string>("foo");
                 var data = new { Foo = source };
                 var data = new { Foo = source };
-                var target = new ExpressionObserver(data, "Foo");
+                var target = ExpressionObserver.Create(data, o => o.Foo);
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
             {
                 var source = new BehaviorSubject<string>("foo");
                 var source = new BehaviorSubject<string>("foo");
                 var data = new { Foo = source };
                 var data = new { Foo = source };
-                var target = new ExpressionObserver(data, "Foo^");
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -60,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             using (var sync = UnitTestSynchronizationContext.Begin())
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
             {
                 var data = new Class1();
                 var data = new Class1();
-                var target = new ExpressionObserver(data, "Next^.Foo");
+                var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo);
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -83,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
             {
                 var source = new BehaviorSubject<string>("foo");
                 var source = new BehaviorSubject<string>("foo");
                 var data = new { Foo = source };
                 var data = new { Foo = source };
-                var target = new ExpressionObserver(data, "Foo^", true);
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -105,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
             {
                 var data1 = new Class1();
                 var data1 = new Class1();
                 var data2 = new Class2("foo");
                 var data2 = new Class2("foo");
-                var target = new ExpressionObserver(data1, "Next^.Foo", true);
+                var target = ExpressionObserver.Create(data1, o => o.Next.StreamBinding().Foo, true);
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -127,8 +128,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             using (var sync = UnitTestSynchronizationContext.Begin())
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
             {
-                var data = new Class2("foo");
-                var target = new ExpressionObserver(data, "Foo^", true);
+                var data = new NotStreamable();
+                var target = ExpressionObserver.Create(data, o => o.StreamBinding());
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -138,7 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     new[]
                     new[]
                     {
                     {
                         new BindingNotification(
                         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)
                             BindingErrorType.Error)
                     },
                     },
                     result);
                     result);
@@ -163,5 +164,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
 
 
             public string Foo { get; }
             public string Foo { get; }
         }
         }
+
+        private class NotStreamable
+        {
+            public object StreamBinding() { throw new InvalidOperationException(); }
+        }
     }
     }
 }
 }

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

@@ -12,6 +12,7 @@ using Avalonia.Data.Core;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
 using Xunit;
 using Xunit;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Avalonia.Markup.Parsers;
 
 
 namespace Avalonia.Base.UnitTests.Data.Core
 namespace Avalonia.Base.UnitTests.Data.Core
 {
 {
@@ -21,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_Property_Value()
         public async Task Should_Get_Simple_Property_Value()
         {
         {
             var data = new { Foo = "foo" };
             var data = new { Foo = "foo" };
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("foo", result);
             Assert.Equal("foo", result);
@@ -33,7 +34,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Get_Simple_Property_Value_Type()
         public void Should_Get_Simple_Property_Value_Type()
         {
         {
             var data = new { Foo = "foo" };
             var data = new { Foo = "foo" };
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
 
 
             target.Subscribe(_ => { });
             target.Subscribe(_ => { });
 
 
@@ -46,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_Property_Value_Null()
         public async Task Should_Get_Simple_Property_Value_Null()
         {
         {
             var data = new { Foo = (string)null };
             var data = new { Foo = (string)null };
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Null(result);
             Assert.Null(result);
@@ -58,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Simple_Property_From_Base_Class()
         public async Task Should_Get_Simple_Property_From_Base_Class()
         {
         {
             var data = new Class3 { Foo = "foo" };
             var data = new Class3 { Foo = "foo" };
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("foo", result);
             Assert.Equal("foo", result);
@@ -69,76 +70,65 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public async Task Should_Return_BindingNotification_Error_For_Root_Null()
         public async Task Should_Return_BindingNotification_Error_For_Root_Null()
         {
         {
-            var data = new Class3 { Foo = "foo" };
-            var target = new ExpressionObserver(default(object), "Foo");
+            var target = ExpressionObserver.Create(default(Class3), o => o.Foo);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal(
             Assert.Equal(
                 new BindingNotification(
                 new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
                         BindingErrorType.Error,
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                         AvaloniaProperty.UnsetValue),
                 result);
                 result);
-
-            GC.KeepAlive(data);
         }
         }
 
 
         [Fact]
         [Fact]
         public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue()
         public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue()
         {
         {
-            var data = new Class3 { Foo = "foo" };
-            var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo");
+            var target = ExpressionObserver.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal(
             Assert.Equal(
                 new BindingNotification(
                 new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
                         BindingErrorType.Error,
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                         AvaloniaProperty.UnsetValue),
                 result);
                 result);
-
-            GC.KeepAlive(data);
         }
         }
 
 
         [Fact]
         [Fact]
         public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null()
         public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null()
         {
         {
-            var data = new Class3 { Foo = "foo" };
-            var target = new ExpressionObserver(Observable.Return(default(object)), "Foo");
+            var target = ExpressionObserver.Create(Observable.Return(default(Class3)), o => o.Foo);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal(
             Assert.Equal(
                 new BindingNotification(
                 new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
                         BindingErrorType.Error,
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                         AvaloniaProperty.UnsetValue),
                 result);
                 result);
-
-            GC.KeepAlive(data);
         }
         }
 
 
         [Fact]
         [Fact]
         public async void Should_Return_BindingNotification_Error_For_Observable_Root_UnsetValue()
         public async void Should_Return_BindingNotification_Error_For_Observable_Root_UnsetValue()
         {
         {
-            var data = new Class3 { Foo = "foo" };
-            var target = new ExpressionObserver(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);
             var result = await target.Take(1);
 
 
             Assert.Equal(
             Assert.Equal(
                 new BindingNotification(
                 new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
                         BindingErrorType.Error,
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                         AvaloniaProperty.UnsetValue),
                 result);
                 result);
-
-            GC.KeepAlive(data);
+            
         }
         }
 
 
         [Fact]
         [Fact]
         public async Task Should_Get_Simple_Property_Chain()
         public async Task Should_Get_Simple_Property_Chain()
         {
         {
             var data = new { Foo = new { Bar = new { Baz = "baz" } } };
             var data = new { Foo = new { Bar = new { Baz = "baz" } } };
-            var target = new ExpressionObserver(data, "Foo.Bar.Baz");
+            var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
             Assert.Equal("baz", result);
             Assert.Equal("baz", result);
@@ -150,7 +140,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Get_Simple_Property_Chain_Type()
         public void Should_Get_Simple_Property_Chain_Type()
         {
         {
             var data = new { Foo = new { Bar = new { Baz = "baz" } } };
             var data = new { Foo = new { Bar = new { Baz = "baz" } } };
-            var target = new ExpressionObserver(data, "Foo.Bar.Baz");
+            var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
 
 
             target.Subscribe(_ => { });
             target.Subscribe(_ => { });
 
 
@@ -159,28 +149,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
             GC.KeepAlive(data);
         }
         }
 
 
-        [Fact]
-        public async Task Should_Return_BindingNotification_Error_For_Broken_Chain()
-        {
-            var data = new { Foo = new { Bar = 1 } };
-            var target = new ExpressionObserver(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]
         [Fact]
         public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
         public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
         {
         {
-            var data = new { Foo = default(object) };
-            var target = new ExpressionObserver(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>();
             var result = new List<object>();
 
 
             target.Subscribe(x => result.Add(x));
             target.Subscribe(x => result.Add(x));
@@ -189,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 new[]
                 new[]
                 {
                 {
                     new BindingNotification(
                     new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo.Bar.Baz", "Foo"),
+                        new MarkupBindingChainException("Null value", "o => o.Foo.Foo.Length", "Foo"),
                         BindingErrorType.Error,
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                         AvaloniaProperty.UnsetValue),
                 },
                 },
@@ -198,22 +171,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
             GC.KeepAlive(data);
         }
         }
 
 
-        [Fact]
-        public void Should_Have_Null_ResultType_For_Broken_Chain()
-        {
-            var data = new { Foo = new { Bar = 1 } };
-            var target = new ExpressionObserver(data, "Foo.Bar.Baz");
-
-            Assert.Null(target.ResultType);
-
-            GC.KeepAlive(data);
-        }
-
         [Fact]
         [Fact]
         public void Should_Track_Simple_Property_Value()
         public void Should_Track_Simple_Property_Value()
         {
         {
             var data = new Class1 { Foo = "foo" };
             var data = new Class1 { Foo = "foo" };
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -232,7 +194,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String()
         public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String()
         {
         {
             var data = new Class1 { Bar = "foo" };
             var data = new Class1 { Bar = "foo" };
-            var target = new ExpressionObserver(data, "Bar");
+            var target = ExpressionObserver.Create(data, o => o.Bar);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -262,7 +224,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_End_Of_Property_Chain_Changing()
         public void Should_Track_End_Of_Property_Chain_Changing()
         {
         {
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
-            var target = new ExpressionObserver(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -283,7 +245,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_Property_Chain_Changing()
         public void Should_Track_Property_Chain_Changing()
         {
         {
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
-            var target = new ExpressionObserver(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -316,7 +278,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 }
                 }
             };
             };
 
 
-            var target = new ExpressionObserver(data, "Next.Next.Bar");
+            var target = ExpressionObserver.Create(data, o => ((o.Next as Class2).Next as Class2).Bar);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -329,7 +291,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 {
                 {
                     "bar",
                     "bar",
                     new BindingNotification(
                     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,
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue),
                         AvaloniaProperty.UnsetValue),
                     "bar"
                     "bar"
@@ -349,7 +311,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
         public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
         {
         {
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
-            var target = new ExpressionObserver(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
             var result = new List<object>();
             var result = new List<object>();
 
 
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
@@ -384,7 +346,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             var data = new Class1 { Foo = "foo" };
             var data = new Class1 { Foo = "foo" };
             var update = new Subject<Unit>();
             var update = new Subject<Unit>();
-            var target = new ExpressionObserver(() => data.Foo, "", update);
+            var target = ExpressionObserver.Create(() => data.Foo, o => o, update);
             var result = new List<object>();
             var result = new List<object>();
 
 
             target.Subscribe(x => result.Add(x));
             target.Subscribe(x => result.Add(x));
@@ -404,7 +366,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var source = scheduler.CreateColdObservable(
             var source = scheduler.CreateColdObservable(
                 OnNext(1, new Class1 { Foo = "foo" }),
                 OnNext(1, new Class1 { Foo = "foo" }),
                 OnNext(2, new Class1 { Foo = "bar" }));
                 OnNext(2, new Class1 { Foo = "bar" }));
-            var target = new ExpressionObserver(source, "Foo");
+            var target = ExpressionObserver.Create(source, o => o.Foo);
             var result = new List<object>();
             var result = new List<object>();
 
 
             using (target.Subscribe(x => result.Add(x)))
             using (target.Subscribe(x => result.Add(x)))
@@ -420,7 +382,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Subscribing_Multiple_Times_Should_Return_Values_To_All()
         public void Subscribing_Multiple_Times_Should_Return_Values_To_All()
         {
         {
             var data = new Class1 { Foo = "foo" };
             var data = new Class1 { Foo = "foo" };
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result1 = new List<object>();
             var result1 = new List<object>();
             var result2 = new List<object>();
             var result2 = new List<object>();
             var result3 = new List<object>();
             var result3 = new List<object>();
@@ -443,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
         public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
         {
         {
             var data = new Class1 { Foo = "foo" };
             var data = new Class1 { Foo = "foo" };
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
 
 
             var sub1 = target.Subscribe(x => { });
             var sub1 = target.Subscribe(x => { });
             var sub2 = target.Subscribe(x => { });
             var sub2 = target.Subscribe(x => { });
@@ -462,7 +424,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Set_Simple_Property_Value()
         public void SetValue_Should_Set_Simple_Property_Value()
         {
         {
             var data = new Class1 { Foo = "foo" };
             var data = new Class1 { Foo = "foo" };
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
 
 
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
@@ -478,7 +440,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Set_Property_At_The_End_Of_Chain()
         public void SetValue_Should_Set_Property_At_The_End_Of_Chain()
         {
         {
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
             var data = new Class1 { Next = new Class2 { Bar = "bar" } };
-            var target = new ExpressionObserver(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
 
 
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
@@ -494,7 +456,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Return_False_For_Missing_Property()
         public void SetValue_Should_Return_False_For_Missing_Property()
         {
         {
             var data = new Class1 { Next = new WithoutBar() };
             var data = new Class1 { Next = new WithoutBar() };
-            var target = new ExpressionObserver(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
 
 
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
@@ -508,7 +470,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Notify_New_Value_With_Inpc()
         public void SetValue_Should_Notify_New_Value_With_Inpc()
         {
         {
             var data = new Class1();
             var data = new Class1();
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
             var result = new List<object>();
             var result = new List<object>();
 
 
             target.Subscribe(x => result.Add(x));
             target.Subscribe(x => result.Add(x));
@@ -523,7 +485,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Notify_New_Value_Without_Inpc()
         public void SetValue_Should_Notify_New_Value_Without_Inpc()
         {
         {
             var data = new Class1();
             var data = new Class1();
-            var target = new ExpressionObserver(data, "Bar");
+            var target = ExpressionObserver.Create(data, o => o.Bar);
             var result = new List<object>();
             var result = new List<object>();
 
 
             target.Subscribe(x => result.Add(x));
             target.Subscribe(x => result.Add(x));
@@ -538,7 +500,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void SetValue_Should_Return_False_For_Missing_Object()
         public void SetValue_Should_Return_False_For_Missing_Object()
         {
         {
             var data = new Class1();
             var data = new Class1();
-            var target = new ExpressionObserver(data, "Next.Bar");
+            var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
 
 
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
@@ -555,7 +517,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var second = new Class1 { Foo = "bar" };
             var second = new Class1 { Foo = "bar" };
             var root = first;
             var root = first;
             var update = new Subject<Unit>();
             var update = new Subject<Unit>();
-            var target = new ExpressionObserver(() => root, "Foo", update);
+            var target = ExpressionObserver.Create(() => root, o => o.Foo, update);
             var result = new List<object>();
             var result = new List<object>();
             var sub = target.Subscribe(x => result.Add(x));
             var sub = target.Subscribe(x => result.Add(x));
 
 
@@ -570,7 +532,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                     "foo",
                     "foo",
                     "bar",
                     "bar",
                     new BindingNotification(
                     new BindingNotification(
-                        new MarkupBindingChainException("Null value", "Foo", string.Empty),
+                        new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
                         BindingErrorType.Error,
                         BindingErrorType.Error,
                         AvaloniaProperty.UnsetValue)
                         AvaloniaProperty.UnsetValue)
                 },
                 },
@@ -589,7 +551,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
             Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
             {
             {
                 var source = new Class1 { Foo = "foo" };
                 var source = new Class1 { Foo = "foo" };
-                var target = new ExpressionObserver(source, "Foo");
+                var target = ExpressionObserver.Create(source, o => o.Foo);
                 return Tuple.Create(target, new WeakReference(source));
                 return Tuple.Create(target, new WeakReference(source));
             };
             };
 
 
@@ -673,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));
         }
         }
     }
     }
 }
 }

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

@@ -5,6 +5,7 @@ using System;
 using System.Reactive.Linq;
 using System.Reactive.Linq;
 using System.Reactive.Subjects;
 using System.Reactive.Subjects;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
 using Xunit;
 using Xunit;
 
 
@@ -16,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Set_Simple_Property_Value()
         public void Should_Set_Simple_Property_Value()
         {
         {
             var data = new { Foo = "foo" };
             var data = new { Foo = "foo" };
-            var target = new ExpressionObserver(data, "Foo");
+            var target = ExpressionObserver.Create(data, o => o.Foo);
 
 
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
@@ -30,7 +31,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Set_Value_On_Simple_Property_Chain()
         public void Should_Set_Value_On_Simple_Property_Chain()
         {
         {
             var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
             var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
-            var target = new ExpressionObserver(data, "Foo.Bar");
+            var target = ExpressionObserver.Create(data, o => o.Foo.Bar);
+
 
 
             using (target.Subscribe(_ => { }))
             using (target.Subscribe(_ => { }))
             {
             {
@@ -44,14 +46,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public void Should_Not_Try_To_Set_Value_On_Broken_Chain()
         public void Should_Not_Try_To_Set_Value_On_Broken_Chain()
         {
         {
             var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
             var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
-            var target = new ExpressionObserver(data, "Foo.Bar");
+            var target = ExpressionObserver.Create(data, o => o.Foo.Bar);
 
 
             // Ensure the ExpressionObserver's subscriptions are kept active.
             // 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>
         /// <summary>
@@ -67,13 +70,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
         {
         {
             var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
             var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
             var rootObservable = new BehaviorSubject<Class1>(data);
             var rootObservable = new BehaviorSubject<Class1>(data);
-            var target = new ExpressionObserver(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
         private class Class1 : NotifyingBase

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

@@ -7,6 +7,7 @@ using System.Reactive.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
 using Xunit;
 using Xunit;
 
 
@@ -15,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
     public class ExpressionObserverTests_Task
     public class ExpressionObserverTests_Task
     {
     {
         [Fact]
         [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())
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
             {
                 var tcs = new TaskCompletionSource<string>();
                 var tcs = new TaskCompletionSource<string>();
                 var data = new { Foo = tcs.Task };
                 var data = new { Foo = tcs.Task };
-                var target = new ExpressionObserver(data, "Foo");
+                var target = ExpressionObserver.Create(data, o => o.Foo);
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             using (var sync = UnitTestSynchronizationContext.Begin())
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
             {
                 var data = new { Foo = Task.FromResult("foo") };
                 var data = new { Foo = Task.FromResult("foo") };
-                var target = new ExpressionObserver(data, "Foo^");
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -59,7 +60,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
             {
                 var tcs = new TaskCompletionSource<Class2>();
                 var tcs = new TaskCompletionSource<Class2>();
                 var data = new Class1(tcs.Task);
                 var data = new Class1(tcs.Task);
-                var target = new ExpressionObserver(data, "Next^.Foo");
+                var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo);
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -79,7 +80,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
             {
                 var tcs = new TaskCompletionSource<string>();
                 var tcs = new TaskCompletionSource<string>();
                 var data = new { Foo = tcs.Task };
                 var data = new { Foo = tcs.Task };
-                var target = new ExpressionObserver(data, "Foo^");
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -105,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             using (var sync = UnitTestSynchronizationContext.Begin())
             using (var sync = UnitTestSynchronizationContext.Begin())
             {
             {
                 var data = new { Foo = TaskFromException(new NotSupportedException()) };
                 var data = new { Foo = TaskFromException(new NotSupportedException()) };
-                var target = new ExpressionObserver(data, "Foo^");
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));
@@ -130,7 +131,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             {
             {
                 var tcs = new TaskCompletionSource<string>();
                 var tcs = new TaskCompletionSource<string>();
                 var data = new { Foo = tcs.Task };
                 var data = new { Foo = tcs.Task };
-                var target = new ExpressionObserver(data, "Foo^", true);
+                var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
                 var result = new List<object>();
                 var result = new List<object>();
 
 
                 var sub = target.Subscribe(x => result.Add(x));
                 var sub = target.Subscribe(x => result.Add(x));

+ 1 - 1
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -515,7 +515,7 @@ namespace Avalonia.Controls.UnitTests
 
 
             public InstancedBinding ItemsSelector(object item)
             public InstancedBinding ItemsSelector(object item)
             {
             {
-                var obs = new ExpressionObserver(item, nameof(Node.Children));
+                var obs = ExpressionObserver.Create(item, o => (o as Node).Children);
                 return InstancedBinding.OneWay(obs);
                 return InstancedBinding.OneWay(obs);
             }
             }
 
 

+ 4 - 4
tests/Avalonia.LeakTests/ExpressionObserverTests.cs

@@ -24,7 +24,7 @@ namespace Avalonia.LeakTests
             Func<ExpressionObserver> run = () =>
             Func<ExpressionObserver> run = () =>
             {
             {
                 var source = new { Foo = new AvaloniaList<string> {"foo", "bar"} };
                 var source = new { Foo = new AvaloniaList<string> {"foo", "bar"} };
-                var target = new ExpressionObserver(source, "Foo");
+                var target = ExpressionObserver.Create(source, o => o.Foo);
 
 
                 target.Subscribe(_ => { });
                 target.Subscribe(_ => { });
                 return target;
                 return target;
@@ -42,7 +42,7 @@ namespace Avalonia.LeakTests
             Func<ExpressionObserver> run = () =>
             Func<ExpressionObserver> run = () =>
             {
             {
                 var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
                 var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
-                var target = new ExpressionObserver(source, "Foo", true);
+                var target = ExpressionObserver.Create(source, o => o.Foo, true);
 
 
                 target.Subscribe(_ => { });
                 target.Subscribe(_ => { });
                 return target;
                 return target;
@@ -60,7 +60,7 @@ namespace Avalonia.LeakTests
             Func<ExpressionObserver> run = () =>
             Func<ExpressionObserver> run = () =>
             {
             {
                 var source = new { Foo = new NonIntegerIndexer() };
                 var source = new { Foo = new NonIntegerIndexer() };
-                var target = new ExpressionObserver(source, "Foo");
+                var target = ExpressionObserver.Create(source, o => o.Foo);
 
 
                 target.Subscribe(_ => { });
                 target.Subscribe(_ => { });
                 return target;
                 return target;
@@ -78,7 +78,7 @@ namespace Avalonia.LeakTests
             Func<ExpressionObserver> run = () =>
             Func<ExpressionObserver> run = () =>
             {
             {
                 var source = new { Foo = new MethodBound() };
                 var source = new { Foo = new MethodBound() };
-                var target = new ExpressionObserver(source, "Foo.A");
+                var target = ExpressionObserver.Create(source, o => (Action)o.Foo.A);
                 target.Subscribe(_ => { });
                 target.Subscribe(_ => { });
                 return target;
                 return target;
             };
             };

+ 29 - 18
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs

@@ -4,16 +4,18 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
+using Avalonia.Markup.Parsers.Nodes;
 using Xunit;
 using Xunit;
 
 
-namespace Avalonia.Base.UnitTests.Data.Core
+namespace Avalonia.Markup.UnitTests.Parsers
 {
 {
-    public class ExpressionNodeBuilderTests
+    public class ExpressionObserverBuilderTests
     {
     {
         [Fact]
         [Fact]
         public void Should_Build_Single_Property()
         public void Should_Build_Single_Property()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("Foo"));
+            var result = ToList(ExpressionObserverBuilder.Parse("Foo"));
 
 
             AssertIsProperty(result[0], "Foo");
             AssertIsProperty(result[0], "Foo");
         }
         }
@@ -21,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public void Should_Build_Underscored_Property()
         public void Should_Build_Underscored_Property()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("_Foo"));
+            var result = ToList(ExpressionObserverBuilder.Parse("_Foo"));
 
 
             AssertIsProperty(result[0], "_Foo");
             AssertIsProperty(result[0], "_Foo");
         }
         }
@@ -29,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public void Should_Build_Property_With_Digits()
         public void Should_Build_Property_With_Digits()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("F0o"));
+            var result = ToList(ExpressionObserverBuilder.Parse("F0o"));
 
 
             AssertIsProperty(result[0], "F0o");
             AssertIsProperty(result[0], "F0o");
         }
         }
@@ -37,7 +39,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public void Should_Build_Property_Chain()
         public void Should_Build_Property_Chain()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz"));
+            var result = ToList(ExpressionObserverBuilder.Parse("Foo.Bar.Baz"));
 
 
             Assert.Equal(3, result.Count);
             Assert.Equal(3, result.Count);
             AssertIsProperty(result[0], "Foo");
             AssertIsProperty(result[0], "Foo");
@@ -48,7 +50,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public void Should_Build_Negated_Property_Chain()
         public void Should_Build_Negated_Property_Chain()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("!Foo.Bar.Baz"));
+            var result = ToList(ExpressionObserverBuilder.Parse("!Foo.Bar.Baz"));
 
 
             Assert.Equal(4, result.Count);
             Assert.Equal(4, result.Count);
             Assert.IsType<LogicalNotNode>(result[0]);
             Assert.IsType<LogicalNotNode>(result[0]);
@@ -60,7 +62,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public void Should_Build_Double_Negated_Property_Chain()
         public void Should_Build_Double_Negated_Property_Chain()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("!!Foo.Bar.Baz"));
+            var result = ToList(ExpressionObserverBuilder.Parse("!!Foo.Bar.Baz"));
 
 
             Assert.Equal(5, result.Count);
             Assert.Equal(5, result.Count);
             Assert.IsType<LogicalNotNode>(result[0]);
             Assert.IsType<LogicalNotNode>(result[0]);
@@ -73,29 +75,29 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public void Should_Build_Indexed_Property()
         public void Should_Build_Indexed_Property()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("Foo[15]"));
+            var result = ToList(ExpressionObserverBuilder.Parse("Foo[15]"));
 
 
             Assert.Equal(2, result.Count);
             Assert.Equal(2, result.Count);
             AssertIsProperty(result[0], "Foo");
             AssertIsProperty(result[0], "Foo");
             AssertIsIndexer(result[1], "15");
             AssertIsIndexer(result[1], "15");
-            Assert.IsType<IndexerNode>(result[1]);
+            Assert.IsType<StringIndexerNode>(result[1]);
         }
         }
 
 
         [Fact]
         [Fact]
         public void Should_Build_Indexed_Property_StringIndex()
         public void Should_Build_Indexed_Property_StringIndex()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("Foo[Key]"));
+            var result = ToList(ExpressionObserverBuilder.Parse("Foo[Key]"));
 
 
             Assert.Equal(2, result.Count);
             Assert.Equal(2, result.Count);
             AssertIsProperty(result[0], "Foo");
             AssertIsProperty(result[0], "Foo");
             AssertIsIndexer(result[1], "Key");
             AssertIsIndexer(result[1], "Key");
-            Assert.IsType<IndexerNode>(result[1]);
+            Assert.IsType<StringIndexerNode>(result[1]);
         }
         }
 
 
         [Fact]
         [Fact]
         public void Should_Build_Multiple_Indexed_Property()
         public void Should_Build_Multiple_Indexed_Property()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("Foo[15,6]"));
+            var result = ToList(ExpressionObserverBuilder.Parse("Foo[15,6]"));
 
 
             Assert.Equal(2, result.Count);
             Assert.Equal(2, result.Count);
             AssertIsProperty(result[0], "Foo");
             AssertIsProperty(result[0], "Foo");
@@ -105,7 +107,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public void Should_Build_Multiple_Indexed_Property_With_Space()
         public void Should_Build_Multiple_Indexed_Property_With_Space()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("Foo[5, 16]"));
+            var result = ToList(ExpressionObserverBuilder.Parse("Foo[5, 16]"));
 
 
             Assert.Equal(2, result.Count);
             Assert.Equal(2, result.Count);
             AssertIsProperty(result[0], "Foo");
             AssertIsProperty(result[0], "Foo");
@@ -115,7 +117,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public void Should_Build_Consecutive_Indexers()
         public void Should_Build_Consecutive_Indexers()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("Foo[15][16]"));
+            var result = ToList(ExpressionObserverBuilder.Parse("Foo[15][16]"));
 
 
             Assert.Equal(3, result.Count);
             Assert.Equal(3, result.Count);
             AssertIsProperty(result[0], "Foo");
             AssertIsProperty(result[0], "Foo");
@@ -126,7 +128,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         [Fact]
         [Fact]
         public void Should_Build_Indexed_Property_In_Chain()
         public void Should_Build_Indexed_Property_In_Chain()
         {
         {
-            var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar[5, 6].Baz"));
+            var result = ToList(ExpressionObserverBuilder.Parse("Foo.Bar[5, 6].Baz"));
 
 
             Assert.Equal(4, result.Count);
             Assert.Equal(4, result.Count);
             AssertIsProperty(result[0], "Foo");
             AssertIsProperty(result[0], "Foo");
@@ -135,6 +137,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
             AssertIsProperty(result[3], "Baz");
             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)
         private void AssertIsProperty(ExpressionNode node, string name)
         {
         {
             Assert.IsType<PropertyAccessorNode>(node);
             Assert.IsType<PropertyAccessorNode>(node);
@@ -145,9 +156,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
 
 
         private void AssertIsIndexer(ExpressionNode node, params string[] args)
         private void AssertIsIndexer(ExpressionNode node, params string[] args)
         {
         {
-            Assert.IsType<IndexerNode>(node);
+            Assert.IsType<StringIndexerNode>(node);
 
 
-            var e = (IndexerNode)node;
+            var e = (StringIndexerNode)node;
             Assert.Equal(e.Arguments.ToArray(), args);
             Assert.Equal(e.Arguments.ToArray(), args);
         }
         }
 
 

+ 12 - 11
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests_Errors.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs

@@ -2,73 +2,74 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using Xunit;
 using Xunit;
 
 
-namespace Avalonia.Base.UnitTests.Data.Core
+namespace Avalonia.Markup.UnitTests.Parsers
 {
 {
-    public class ExpressionNodeBuilderTests_Errors
+    public class ExpressionObserverBuilderTests_Errors
     {
     {
         [Fact]
         [Fact]
         public void Identifier_Cannot_Start_With_Digit()
         public void Identifier_Cannot_Start_With_Digit()
         {
         {
             Assert.Throws<ExpressionParseException>(
             Assert.Throws<ExpressionParseException>(
-                () => ExpressionNodeBuilder.Build("1Foo"));
+                () => ExpressionObserverBuilder.Parse("1Foo"));
         }
         }
 
 
         [Fact]
         [Fact]
         public void Identifier_Cannot_Start_With_Symbol()
         public void Identifier_Cannot_Start_With_Symbol()
         {
         {
             Assert.Throws<ExpressionParseException>(
             Assert.Throws<ExpressionParseException>(
-                () => ExpressionNodeBuilder.Build("Foo.%Bar"));
+                () => ExpressionObserverBuilder.Parse("Foo.%Bar"));
         }
         }
 
 
         [Fact]
         [Fact]
         public void Expression_Cannot_End_With_Period()
         public void Expression_Cannot_End_With_Period()
         {
         {
             Assert.Throws<ExpressionParseException>(
             Assert.Throws<ExpressionParseException>(
-                () => ExpressionNodeBuilder.Build("Foo.Bar."));
+                () => ExpressionObserverBuilder.Parse("Foo.Bar."));
         }
         }
 
 
         [Fact]
         [Fact]
         public void Expression_Cannot_Have_Empty_Indexer()
         public void Expression_Cannot_Have_Empty_Indexer()
         {
         {
             Assert.Throws<ExpressionParseException>(
             Assert.Throws<ExpressionParseException>(
-                () => ExpressionNodeBuilder.Build("Foo.Bar[]"));
+                () => ExpressionObserverBuilder.Parse("Foo.Bar[]"));
         }
         }
 
 
         [Fact]
         [Fact]
         public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer()
         public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer()
         {
         {
             Assert.Throws<ExpressionParseException>(
             Assert.Throws<ExpressionParseException>(
-                () => ExpressionNodeBuilder.Build("Foo.Bar[,3,4]"));
+                () => ExpressionObserverBuilder.Parse("Foo.Bar[,3,4]"));
         }
         }
 
 
         [Fact]
         [Fact]
         public void Expression_Cannot_Have_Extra_Comma_In_Indexer()
         public void Expression_Cannot_Have_Extra_Comma_In_Indexer()
         {
         {
             Assert.Throws<ExpressionParseException>(
             Assert.Throws<ExpressionParseException>(
-                () => ExpressionNodeBuilder.Build("Foo.Bar[3,,4]"));
+                () => ExpressionObserverBuilder.Parse("Foo.Bar[3,,4]"));
         }
         }
 
 
         [Fact]
         [Fact]
         public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer()
         public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer()
         {
         {
             Assert.Throws<ExpressionParseException>(
             Assert.Throws<ExpressionParseException>(
-                () => ExpressionNodeBuilder.Build("Foo.Bar[3,4,]"));
+                () => ExpressionObserverBuilder.Parse("Foo.Bar[3,4,]"));
         }
         }
 
 
         [Fact]
         [Fact]
         public void Expression_Cannot_Have_Digit_After_Indexer()
         public void Expression_Cannot_Have_Digit_After_Indexer()
         {
         {
             Assert.Throws<ExpressionParseException>(
             Assert.Throws<ExpressionParseException>(
-                () => ExpressionNodeBuilder.Build("Foo.Bar[3,4]5"));
+                () => ExpressionObserverBuilder.Parse("Foo.Bar[3,4]5"));
         }
         }
 
 
         [Fact]
         [Fact]
         public void Expression_Cannot_Have_Letter_After_Indexer()
         public void Expression_Cannot_Have_Letter_After_Indexer()
         {
         {
             Assert.Throws<ExpressionParseException>(
             Assert.Throws<ExpressionParseException>(
-                () => ExpressionNodeBuilder.Build("Foo.Bar[3,4]A"));
+                () => ExpressionObserverBuilder.Parse("Foo.Bar[3,4]A"));
         }
         }
     }
     }
 }
 }

+ 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);
+                }
+            }
+        }
+    }
+}

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

@@ -1,5 +1,6 @@
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
@@ -8,9 +9,9 @@ using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Xunit;
 using Xunit;
 
 
-namespace Avalonia.Base.UnitTests.Data.Core
+namespace Avalonia.Markup.UnitTests.Parsers
 {
 {
-    public class ExpressionObserverTests_Method
+    public class ExpressionObserverBuilderTests_Method
     {
     {
         private class TestObject
         private class TestObject
         {
         {
@@ -30,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Method()
         public async Task Should_Get_Method()
         {
         {
             var data = new TestObject();
             var data = new TestObject();
-            var observer = new ExpressionObserver(data, nameof(TestObject.MethodWithoutReturn));
+            var observer = ExpressionObserverBuilder.Build(data, nameof(TestObject.MethodWithoutReturn));
             var result = await observer.Take(1);
             var result = await observer.Take(1);
 
 
             Assert.NotNull(result);
             Assert.NotNull(result);
@@ -46,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, Type expectedType)
         public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, Type expectedType)
         {
         {
             var data = new TestObject();
             var data = new TestObject();
-            var observer = new ExpressionObserver(data, methodName);
+            var observer = ExpressionObserverBuilder.Build(data, methodName);
             var result = await observer.Take(1);
             var result = await observer.Take(1);
 
 
             Assert.IsType(expectedType, result);
             Assert.IsType(expectedType, result);
@@ -58,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Can_Call_Method_Returned_From_Observer()
         public async Task Can_Call_Method_Returned_From_Observer()
         {
         {
             var data = new TestObject();
             var data = new TestObject();
-            var observer = new ExpressionObserver(data, nameof(TestObject.MethodWithReturnAndParameters));
+            var observer = ExpressionObserverBuilder.Build(data, nameof(TestObject.MethodWithReturnAndParameters));
             var result = await observer.Take(1);
             var result = await observer.Take(1);
 
 
             var callback = (Func<int, int>)result;
             var callback = (Func<int, int>)result;
@@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
         public async Task Should_Return_Error_Notification_If_Too_Many_Parameters(string methodName)
         public async Task Should_Return_Error_Notification_If_Too_Many_Parameters(string methodName)
         {
         {
             var data = new TestObject();
             var data = new TestObject();
-            var observer = new ExpressionObserver(data, methodName);
+            var observer = ExpressionObserverBuilder.Build(data, methodName);
             var result = await observer.Take(1);
             var result = await observer.Take(1);
 
 
             Assert.IsType<BindingNotification>(result);
             Assert.IsType<BindingNotification>(result);

+ 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);
+        }
+    }
+}

+ 1 - 1
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@@ -6,7 +6,7 @@ using Avalonia.Markup.Parsers;
 using Sprache;
 using Sprache;
 using Xunit;
 using Xunit;
 
 
-namespace Avalonia.Markup.UnitTest.Parsers
+namespace Avalonia.Markup.UnitTests.Parsers
 {
 {
     public class SelectorGrammarTests
     public class SelectorGrammarTests
     {
     {

+ 1 - 1
tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs

@@ -3,7 +3,7 @@ using Avalonia.Controls;
 using Avalonia.Markup.Parsers;
 using Avalonia.Markup.Parsers;
 using Xunit;
 using Xunit;
 
 
-namespace Avalonia.Markup.Xaml.UnitTests.Parsers
+namespace Avalonia.Markup.UnitTests.Parsers
 {
 {
     public class SelectorParserTests
     public class SelectorParserTests
     {
     {