From e8c32bf801a6f36270155657b4665821019f986a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 17 May 2018 17:39:46 -0500 Subject: [PATCH 01/12] Preliminary support. Indexers require more work since the compiler doesn't generate IndexExpressions (they weren't in S.L.Expressions v1 so they aren't auto-genned). --- .../Data/ExpressionNodeBuilder.cs | 8 + .../Data/ExpressionObserver.cs | 92 ++++++++-- .../Data/ExpressionParseException.cs | 4 +- .../Data/IndexerExpressionNode.cs | 61 +++++++ .../Avalonia.Markup/Data/IndexerNode.cs | 80 ++------- .../Avalonia.Markup/Data/IndexerNodeBase.cs | 87 ++++++++++ .../Data/Parsers/ExpressionTreeParser.cs | 34 ++++ .../Parsers/ExpressionVisitorNodeBuilder.cs | 158 ++++++++++++++++++ .../ExpressionObserverTests_ExpressionTree.cs | 111 ++++++++++++ 9 files changed, 555 insertions(+), 80 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs create mode 100644 src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs create mode 100644 src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs create mode 100644 src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs create mode 100644 tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs index 013299c1d72..e19259c6ed5 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq.Expressions; using Avalonia.Markup.Data.Parsers; namespace Avalonia.Markup.Data @@ -26,5 +27,12 @@ public static ExpressionNode Build(string expression, bool enableValidation = fa return node; } + + public static ExpressionNode Build(LambdaExpression expression, bool enableValidation = false) + { + var parser = new ExpressionTreeParser(enableValidation); + + return parser.Parse(expression); + } } } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index dd9718a0f63..127b338008d 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -72,20 +73,36 @@ public ExpressionObserver( string expression, bool enableDataValidation = false, string description = null) + : this(root, Parse(expression, enableDataValidation), description ?? expression) { Contract.Requires(expression != null); + Expression = expression; + } + private ExpressionObserver( + object root, + ExpressionNode node, + string description = null) + { if (root == AvaloniaProperty.UnsetValue) { root = null; } - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + _node = node; + Description = description; _root = new WeakReference(root); } + public static ExpressionObserver CreateFromExpression( + T root, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString()); + } + /// /// Initializes a new instance of the class. /// @@ -100,15 +117,38 @@ public ExpressionObserver( string expression, bool enableDataValidation = false, string description = null) + : this(rootObservable, Parse(expression, enableDataValidation), description ?? expression) { Contract.Requires(rootObservable != null); Contract.Requires(expression != null); Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); - _finished = new Subject(); + } + + private ExpressionObserver( + IObservable rootObservable, + ExpressionNode node, + string description) + { + Contract.Requires(rootObservable != null); + + _node = node; + Description = description; _root = rootObservable; + _finished = new Subject(); + } + + public static ExpressionObserver CreateFromExpression( + IObservable rootObservable, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootObservable != null); + return new ExpressionObserver( + rootObservable.Select(o => (object)o), + Parse(expression, enableDataValidation), + description ?? expression.ToString()); } /// @@ -127,19 +167,44 @@ public ExpressionObserver( IObservable update, bool enableDataValidation = false, string description = null) + : this(rootGetter, Parse(expression, enableDataValidation), update, description ?? expression) { - Contract.Requires(rootGetter != null); Contract.Requires(expression != null); - Contract.Requires(update != null); Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); - _finished = new Subject(); + } + + private ExpressionObserver( + Func rootGetter, + ExpressionNode node, + IObservable update, + string description) + { + Contract.Requires(rootGetter != null); + Contract.Requires(update != null); + Description = description; + _node = node; + _finished = new Subject(); _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } + + public static ExpressionObserver CreateFromExpression( + Func rootGetter, + Expression> expression, + IObservable update, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootGetter != null); + + return new ExpressionObserver( + () => rootGetter(), + Parse(expression, enableDataValidation), + update, + description ?? expression.ToString()); + } /// /// Attempts to set the value of a property expression. @@ -238,6 +303,11 @@ private static ExpressionNode Parse(string expression, bool enableDataValidation } } + private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) + { + return ExpressionNodeBuilder.Build(expression, enableDataValidation); + } + private static object ToWeakReference(object o) { return o is BindingNotification ? o : new WeakReference(o); diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs b/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs index d06bdd1e527..3ef225e70a7 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs @@ -17,8 +17,8 @@ public class ExpressionParseException : Exception /// /// The column position of the error. /// The exception message. - public ExpressionParseException(int column, string message) - : base(message) + public ExpressionParseException(int column, string message, Exception innerException = null) + : base(message, innerException) { Column = column; } diff --git a/src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs new file mode 100644 index 00000000000..b296badb860 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Text; +using Avalonia.Data; + +namespace Avalonia.Markup.Data +{ + 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); + this.expression = expression.Update(parameter, expression.Arguments); + + getDelegate = Expression.Lambda(this.expression, parameter).Compile(); + + var valueParameter = Expression.Parameter(expression.Type); + + setDelegate = Expression.Lambda(Expression.Assign(this.expression, valueParameter), parameter, valueParameter).Compile(); + + firstArgumentDelegate = Expression.Lambda(this.expression.Arguments[0], parameter).Compile(); + } + + public override Type PropertyType => expression.Type; + + public override string Description => expression.ToString(); + + public override bool SetTargetValue(object value, BindingPriority priority) + { + try + { + setDelegate.DynamicInvoke(Target.Target, value); + return true; + } + catch (Exception) + { + return false; + } + } + + protected override object GetValue(object target) + { + return getDelegate.DynamicInvoke(target); + } + + protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + { + return expression.Indexer.Name == e.PropertyName; + } + + protected override int? TryGetFirstArgumentAsInt() => firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; + } +} diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs index 4e2914a1488..09ce2b85e9d 100644 --- a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs @@ -15,7 +15,7 @@ namespace Avalonia.Markup.Data { - internal class IndexerNode : ExpressionNode, ISettableNode + internal class IndexerNode : IndexerNodeBase { public IndexerNode(IList arguments) { @@ -24,35 +24,7 @@ public IndexerNode(IList arguments) public override string Description => "[" + string.Join(",", Arguments) + "]"; - protected override IObservable StartListeningCore(WeakReference reference) - { - var target = reference.Target; - var incc = target as INotifyCollectionChanged; - var inpc = target as INotifyPropertyChanged; - var inputs = new List>(); - - if (incc != null) - { - inputs.Add(WeakObservable.FromEventPattern( - incc, - nameof(incc.CollectionChanged)) - .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) - .Select(_ => GetValue(target))); - } - - if (inpc != null) - { - inputs.Add(WeakObservable.FromEventPattern( - inpc, - nameof(inpc.PropertyChanged)) - .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) - .Select(_ => GetValue(target))); - } - - return Observable.Merge(inputs).StartWith(GetValue(target)); - } - - public bool SetTargetValue(object value, BindingPriority priority) + public override bool SetTargetValue(object value, BindingPriority priority) { var typeInfo = Target.Target.GetType().GetTypeInfo(); var list = Target.Target as IList; @@ -154,9 +126,9 @@ private bool SetValueInArray(Array array, int[] indicies, object value) public IList Arguments { get; } - public 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 list = target as IList; @@ -309,45 +281,19 @@ private bool ValidBounds(int[] indicies, Array array) } } - 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; } } } diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs b/src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs new file mode 100644 index 00000000000..d3a4a818fea --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs @@ -0,0 +1,87 @@ +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.Markup.Data +{ + abstract class IndexerNodeBase : ExpressionNode, ISettableNode + { + protected override IObservable StartListeningCore(WeakReference reference) + { + var target = reference.Target; + var inputs = new List>(); + + if (target is INotifyCollectionChanged incc) + { + inputs.Add(WeakObservable.FromEventPattern( + incc, + nameof(incc.CollectionChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + if (target is INotifyPropertyChanged inpc) + { + inputs.Add(WeakObservable.FromEventPattern( + inpc, + nameof(inpc.PropertyChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + return inputs.Merge().StartWith(GetValue(target)); + } + + public abstract bool SetTargetValue(object value, BindingPriority priority); + + public abstract Type PropertyType { get; } + + 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); + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs new file mode 100644 index 00000000000..9e225ffcc55 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace Avalonia.Markup.Data.Parsers +{ + class ExpressionTreeParser + { + private readonly bool enableDataValidation; + + public ExpressionTreeParser(bool enableDataValidation) + { + this.enableDataValidation = enableDataValidation; + } + + public ExpressionNode Parse(Expression expr) + { + 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(); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs new file mode 100644 index 00000000000..0126d310981 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace Avalonia.Markup.Data.Parsers +{ + class ExpressionVisitorNodeBuilder : ExpressionVisitor + { + private static PropertyInfo AvaloniaObjectIndexer; + + private readonly bool enableDataValidation; + + static ExpressionVisitorNodeBuilder() + { + AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) }); + } + + public List Nodes { get; } + + public ExpressionVisitorNodeBuilder(bool enableDataValidation) + { + this.enableDataValidation = enableDataValidation; + Nodes = new List(); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType != ExpressionType.Not || node.Type != typeof(bool)) + { + throw new ExpressionParseException(0, $"Invalid unary operation {node.NodeType} in binding expression"); + } + + Nodes.Add(new LogicalNotNode()); + + return base.VisitUnary(node); + } + + protected override Expression VisitMember(MemberExpression node) + { + Nodes.Add(new PropertyAccessorNode(node.Member.Name, enableDataValidation)); + return base.VisitMember(node); + } + + protected override Expression VisitIndex(IndexExpression node) + { + if (node.Indexer == AvaloniaObjectIndexer) + { + var property = GetArgumentExpressionValue(node.Arguments[0]); + Nodes.Add(new PropertyAccessorNode($"{property.OwnerType.Name}.{property.Name}", enableDataValidation)); + } + else + { + Nodes.Add(new IndexerExpressionNode(node)); + } + + return node; + } + + private T GetArgumentExpressionValue(Expression expr) + { + try + { + return Expression.Lambda>(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 base.VisitBinary(node); + } + 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) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + 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}."); + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs new file mode 100644 index 00000000000..3fdd9ffc10f --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Data +{ + public class ExpressionObserverTests_ExpressionTree + { + [Fact] + public async Task IdentityExpression_Creates_IdentityObserver() + { + var target = new object(); + + var observer = ExpressionObserver.CreateFromExpression(target, o => o); + + Assert.Equal(target, await observer.Take(1)); + } + + [Fact] + public async Task Property_Access_Expression_Observes_Property() + { + var target = new Class1(); + + var observer = ExpressionObserver.CreateFromExpression(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.CreateFromExpression(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.CreateFromExpression(data, o => o[0]); + + Assert.Equal(data[0], await target.Take(1)); + } + + [Fact] + public async Task Indexer_Accessor_Can_Read_Complex_Index() + { + var data = new Dictionary(); + + var key = new object(); + + data.Add(key, new object()); + + var target = ExpressionObserver.CreateFromExpression(data, o => o[key]); + + Assert.Equal(data[key], await target.Take(1)); + } + + [Fact] + public void Indexer_Can_Set_Value() + { + var data = new[] { 1, 2, 3, 4 }; + + var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue(2)); + } + + GC.KeepAlive(data); + } + + private class Class1 : NotifyingBase + { + private string _foo; + + public string Foo + { + get { return _foo; } + set + { + _foo = value; + RaisePropertyChanged(nameof(Foo)); + } + } + } + } +} From bf6375fe266f3825e59eaf5f020c9bfbe955f45a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 29 May 2018 16:49:08 -0500 Subject: [PATCH 02/12] Fix indexer and casting expressions. --- .../Parsers/ExpressionVisitorNodeBuilder.cs | 49 ++++++++++-- .../ExpressionObserverTests_ExpressionTree.cs | 80 +++++++++++++++++++ 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index 8564bf5111b..5affe227e17 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -28,24 +29,43 @@ public ExpressionVisitorNodeBuilder(bool enableDataValidation) protected override Expression VisitUnary(UnaryExpression node) { - if (node.NodeType != ExpressionType.Not || node.Type != typeof(bool)) + if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool)) { - throw new ExpressionParseException(0, $"Invalid unary operation {node.NodeType} in binding expression"); + 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"); } - - Nodes.Add(new LogicalNotNode()); return base.VisitUnary(node); } protected override Expression VisitMember(MemberExpression node) { + var visited = base.VisitMember(node); Nodes.Add(new PropertyAccessorNode(node.Member.Name, enableDataValidation)); - return base.VisitMember(node); + return visited; } protected override Expression VisitIndex(IndexExpression node) { + var visited = base.VisitIndex(node); if (node.Indexer == AvaloniaObjectIndexer) { var property = GetArgumentExpressionValue(node.Arguments[0]); @@ -56,7 +76,7 @@ protected override Expression VisitIndex(IndexExpression node) Nodes.Add(new IndexerExpressionNode(node)); } - return node; + return visited; } private T GetArgumentExpressionValue(Expression expr) @@ -75,7 +95,8 @@ protected override Expression VisitBinary(BinaryExpression node) { if (node.NodeType == ExpressionType.ArrayIndex) { - return base.VisitBinary(node); + base.VisitBinary(node); + return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right })); } throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); } @@ -137,9 +158,23 @@ protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) protected override Expression VisitMethodCall(MethodCallExpression node) { + base.VisitMethodCall(node); + var property = TryGetPropertyFromMethod(node.Method); + + if (property != null) + { + return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); } + 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}."); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs index 1ec4bdb4f50..ebf3ca2a490 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs @@ -19,6 +19,7 @@ public async Task IdentityExpression_Creates_IdentityObserver() var observer = ExpressionObserver.CreateFromExpression(target, o => o); Assert.Equal(target, await observer.Take(1)); + GC.KeepAlive(target); } [Fact] @@ -62,6 +63,18 @@ public async Task Indexer_Accessor_Can_Read_Value() var target = ExpressionObserver.CreateFromExpression(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 { 1, 2, 3, 4 }; + + var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + + Assert.Equal(data[0], await target.Take(1)); + GC.KeepAlive(data); } [Fact] @@ -76,6 +89,8 @@ public async Task Indexer_Accessor_Can_Read_Complex_Index() var target = ExpressionObserver.CreateFromExpression(data, o => o[key]); Assert.Equal(data[key], await target.Take(1)); + + GC.KeepAlive(data); } [Fact] @@ -93,6 +108,62 @@ public void Indexer_Can_Set_Value() GC.KeepAlive(data); } + [Fact] + public async Task Inheritance_Casts_Should_Be_Ignored() + { + NotifyingBase test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.CreateFromExpression(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(() => ExpressionObserver.CreateFromExpression(test, o => (double)o)); + } + + [Fact] + public async Task As_Operator_Should_Be_Ignored() + { + NotifyingBase test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.CreateFromExpression(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.CreateFromExpression(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.CreateFromExpression(test, o => o.Foo.Length); + + Assert.Equal(test.Foo.Length, await target.Take(1)); + + GC.KeepAlive(test); + } + private class Class1 : NotifyingBase { private string _foo; @@ -107,5 +178,14 @@ public string Foo } } } + + + private class Class2 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", defaultValue: "foo"); + + public string ClrProperty { get; } = "clr-property"; + } } } From d2823110318865693625463f92eaf42c64ed98c4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 31 May 2018 18:39:03 -0500 Subject: [PATCH 03/12] Fix bug in ExpressionVisitorNodeBuilder. --- .../Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index 5affe227e17..433cfd1889e 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -65,7 +65,8 @@ protected override Expression VisitMember(MemberExpression node) protected override Expression VisitIndex(IndexExpression node) { - var visited = base.VisitIndex(node); + Visit(node.Object); + if (node.Indexer == AvaloniaObjectIndexer) { var property = GetArgumentExpressionValue(node.Arguments[0]); @@ -76,7 +77,7 @@ protected override Expression VisitIndex(IndexExpression node) Nodes.Add(new IndexerExpressionNode(node)); } - return visited; + return node; } private T GetArgumentExpressionValue(Expression expr) @@ -158,7 +159,6 @@ protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) protected override Expression VisitMethodCall(MethodCallExpression node) { - base.VisitMethodCall(node); var property = TryGetPropertyFromMethod(node.Method); if (property != null) From af186e35296d396ed74d902c466cda1f4c020741 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 7 Jun 2018 14:44:41 -0500 Subject: [PATCH 04/12] Move string-based binding paths up to Avalonia.Markup. Make the LINQ Expression paths and raw ExpressionNodes (now public) the primarily supported syntax. --- .../Data/Core/AvaloniaPropertyAccessorNode.cs | 45 +++++++++++ .../Data/Core/EmptyExpressionNode.cs | 2 +- src/Avalonia.Base/Data/Core/ExpressionNode.cs | 2 +- .../Data/Core/ExpressionNodeBuilder.cs | 38 ---------- .../Data/Core/ExpressionObserver.cs | 74 +++--------------- .../Data/Core/IndexerNodeBase.cs | 2 +- src/Avalonia.Base/Data/Core/LogicalNotNode.cs | 2 +- .../Parsers/ExpressionVisitorNodeBuilder.cs | 11 ++- .../Data/Core/PropertyAccessorNode.cs | 2 +- src/Avalonia.Base/Data/Core/StreamNode.cs | 2 +- .../Converters/SelectorTypeConverter.cs | 2 +- .../MarkupExtensions/BindingExtension.cs | 1 + .../Templates/MemberSelector.cs | 3 +- .../Templates/TreeDataTemplate.cs | 3 +- src/Markup/Avalonia.Markup/Data/Binding.cs | 33 +++++--- .../Markup}/Parsers/ArgumentListParser.cs | 3 +- .../Parsers/ExpressionObserverBuilder.cs | 75 +++++++++++++++++++ .../Markup}/Parsers/ExpressionParser.cs | 30 ++++++-- .../Markup}/Parsers/IdentifierParser.cs | 2 +- .../Parsers/Nodes/StringIndexerNode.cs} | 7 +- .../Avalonia.Markup/Markup}/Parsers/Reader.cs | 2 +- .../Markup/Parsers/SelectorParser.cs | 4 +- .../Data/Core/BindingExpressionTests.cs | 41 +++++----- ...xpressionObserverTests_AttachedProperty.cs | 31 ++++++-- ...xpressionObserverTests_AvaloniaProperty.cs | 9 ++- .../ExpressionObserverTests_DataValidation.cs | 15 ++-- .../Core/ExpressionObserverTests_Indexer.cs | 45 +++++------ .../Core/ExpressionObserverTests_Lifetime.cs | 13 ++-- .../Core/ExpressionObserverTests_Method.cs | 9 ++- .../Core/ExpressionObserverTests_Negation.cs | 19 ++--- .../ExpressionObserverTests_Observable.cs | 13 ++-- .../Core/ExpressionObserverTests_Property.cs | 63 ++++++++-------- .../Core/ExpressionObserverTests_SetValue.cs | 9 ++- .../Data/Core/ExpressionObserverTests_Task.cs | 13 ++-- .../TreeViewTests.cs | 2 +- .../ExpressionObserverTests.cs | 8 +- .../Parsers}/ExpressionNodeBuilderTests.cs | 38 +++++----- .../ExpressionNodeBuilderTests_Errors.cs | 23 +++--- .../Parsers/SelectorGrammarTests.cs | 2 +- .../Parsers/SelectorParserTests.cs | 2 +- 40 files changed, 403 insertions(+), 297 deletions(-) create mode 100644 src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs delete mode 100644 src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs rename src/{Avalonia.Base/Data/Core => Markup/Avalonia.Markup/Markup}/Parsers/ArgumentListParser.cs (96%) create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs rename src/{Avalonia.Base/Data/Core => Markup/Avalonia.Markup/Markup}/Parsers/ExpressionParser.cs (84%) rename src/{Avalonia.Base/Data/Core => Markup/Avalonia.Markup/Markup}/Parsers/IdentifierParser.cs (97%) rename src/{Avalonia.Base/Data/Core/IndexerNode.cs => Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs} (98%) rename src/{Avalonia.Base/Data/Core => Markup/Avalonia.Markup/Markup}/Parsers/Reader.cs (96%) rename tests/{Avalonia.Base.UnitTests/Data/Core => Avalonia.Markup.UnitTests/Parsers}/ExpressionNodeBuilderTests.cs (75%) rename tests/{Avalonia.Base.UnitTests/Data/Core => Avalonia.Markup.UnitTests/Parsers}/ExpressionNodeBuilderTests_Errors.cs (67%) diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs new file mode 100644 index 00000000000..18e853722dc --- /dev/null +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; + +namespace Avalonia.Data.Core +{ + public class AvaloniaPropertyAccessorNode : ExpressionNode, ISettableNode + { + 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 Type PropertyType => _property.PropertyType; + + public bool SetTargetValue(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 IObservable StartListeningCore(WeakReference reference) + { + return (reference.Target as IAvaloniaObject)?.GetWeakObservable(_property) ?? Observable.Empty(); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 93e0d5947ad..24d40900839 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -6,7 +6,7 @@ namespace Avalonia.Data.Core { - internal class EmptyExpressionNode : ExpressionNode + public class EmptyExpressionNode : ExpressionNode { public override string Description => "."; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index ae70cacdba1..980a97b91cb 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -9,7 +9,7 @@ namespace Avalonia.Data.Core { - internal abstract class ExpressionNode : ISubject + public abstract class ExpressionNode : ISubject { protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs b/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs deleted file mode 100644 index b2262f32c39..00000000000 --- a/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs +++ /dev/null @@ -1,38 +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; -using System.Linq.Expressions; - -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; - } - - public static ExpressionNode Build(LambdaExpression expression, bool enableValidation = false) - { - var parser = new ExpressionTreeParser(enableValidation); - - return parser.Parse(expression); - } - } -} diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 32011e8ee91..9a3e2995755 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -9,6 +9,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia.Data; +using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -63,23 +64,11 @@ public class ExpressionObserver : ObservableBase, IDescription /// Initializes a new instance of the class. /// /// The root object. - /// The expression. - /// Whether data validation should be enabled. + /// The expression. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. If null, will be used. /// public ExpressionObserver( - object root, - string expression, - bool enableDataValidation = false, - string description = null) - : this(root, Parse(expression, enableDataValidation), description ?? expression) - { - Contract.Requires(expression != null); - Expression = expression; - } - - private ExpressionObserver( object root, ExpressionNode node, string description = null) @@ -107,25 +96,11 @@ public static ExpressionObserver CreateFromExpression( /// Initializes a new instance of the class. /// /// An observable which provides the root object. - /// The expression. - /// Whether data validation should be enabled. + /// The expression. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. If null, will be used. /// public ExpressionObserver( - IObservable rootObservable, - string expression, - bool enableDataValidation = false, - string description = null) - : this(rootObservable, Parse(expression, enableDataValidation), description ?? expression) - { - Contract.Requires(rootObservable != null); - Contract.Requires(expression != null); - - Expression = expression; - } - - private ExpressionObserver( IObservable rootObservable, ExpressionNode node, string description) @@ -137,7 +112,7 @@ private ExpressionObserver( _root = rootObservable; _finished = new Subject(); } - + public static ExpressionObserver CreateFromExpression( IObservable rootObservable, Expression> expression, @@ -155,26 +130,12 @@ public static ExpressionObserver CreateFromExpression( /// Initializes a new instance of the class. /// /// A function which gets the root object. - /// The expression. + /// The expression. /// An observable which triggers a re-read of the getter. - /// Whether data validation should be enabled. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. If null, will be used. /// public ExpressionObserver( - Func rootGetter, - string expression, - IObservable update, - bool enableDataValidation = false, - string description = null) - : this(rootGetter, Parse(expression, enableDataValidation), update, description ?? expression) - { - Contract.Requires(expression != null); - - Expression = expression; - } - - private ExpressionObserver( Func rootGetter, ExpressionNode node, IObservable update, @@ -189,8 +150,8 @@ private ExpressionObserver( _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } - - public static ExpressionObserver CreateFromExpression( + + public static ExpressionObserver Create( Func rootGetter, Expression> expression, IObservable update, @@ -291,21 +252,10 @@ protected override IDisposable SubscribeCore(IObserver observer) return _result.Subscribe(observer); } - private static ExpressionNode Parse(string expression, bool enableDataValidation) - { - if (!string.IsNullOrWhiteSpace(expression)) - { - return ExpressionNodeBuilder.Build(expression, enableDataValidation); - } - else - { - return new EmptyExpressionNode(); - } - } - private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) { - return ExpressionNodeBuilder.Build(expression, enableDataValidation); + var parser = new ExpressionTreeParser(enableDataValidation); + return parser.Parse(expression); } private static object ToWeakReference(object o) diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index e33beca6aad..6dc7d61168c 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -13,7 +13,7 @@ namespace Avalonia.Data.Core { - abstract class IndexerNodeBase : ExpressionNode, ISettableNode + public abstract class IndexerNodeBase : ExpressionNode, ISettableNode { protected override IObservable StartListeningCore(WeakReference reference) { diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index f277005cecd..20f1bcd21e6 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Data.Core { - internal class LogicalNotNode : ExpressionNode, ITransformNode + public class LogicalNotNode : ExpressionNode, ITransformNode { public override string Description => "!"; diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index 433cfd1889e..614faf00618 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -11,12 +11,14 @@ namespace Avalonia.Data.Core.Parsers class ExpressionVisitorNodeBuilder : ExpressionVisitor { 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 Nodes { get; } @@ -70,7 +72,7 @@ protected override Expression VisitIndex(IndexExpression node) if (node.Indexer == AvaloniaObjectIndexer) { var property = GetArgumentExpressionValue(node.Arguments[0]); - Nodes.Add(new PropertyAccessorNode($"{property.OwnerType.Name}.{property.Name}", enableDataValidation)); + Nodes.Add(new AvaloniaPropertyAccessorNode(property, enableDataValidation)); } else { @@ -166,6 +168,13 @@ protected override Expression VisitMethodCall(MethodCallExpression node) return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); } + if (node.Method == CreateDelegateMethod) + { + var visited = Visit(node.Arguments[1]); + Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue(node.Object).Name, enableDataValidation)); + return visited; + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 4dbff4602fe..39148e5bfd3 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core { - internal class PropertyAccessorNode : ExpressionNode, ISettableNode + public class PropertyAccessorNode : ExpressionNode, ISettableNode { private readonly bool _enableValidation; private IPropertyAccessor _accessor; diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 187c79af49b..8cdece98602 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Data.Core { - internal class StreamNode : ExpressionNode + public class StreamNode : ExpressionNode { public override string Description => "^"; diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs index fb0131a9b42..54234fe406d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs @@ -19,7 +19,7 @@ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceT 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); } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 98203deebee..c3229d814c6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -37,6 +37,7 @@ public override object ProvideValue(IServiceProvider serviceProvider) return new Binding { + TypeResolver = descriptorContext.ResolveType, Converter = Converter, ConverterParameter = ConverterParameter, ElementName = pathInfo.ElementName ?? ElementName, diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs index aa3c3599534..fa91ab60ff7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using System; using System.Reactive.Linq; @@ -37,7 +38,7 @@ public object Select(object o) return o; } - var expression = new ExpressionObserver(o, MemberName); + var expression = ExpressionObserverBuilder.Build(o, MemberName); object result = AvaloniaProperty.UnsetValue; expression.Subscribe(x => result = x); diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index a733ef761c9..bd2b9d2efdc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -8,6 +8,7 @@ using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Markup.Data; +using Avalonia.Markup.Parsers; using Avalonia.Metadata; namespace Avalonia.Markup.Xaml.Templates @@ -41,7 +42,7 @@ public InstancedBinding ItemsSelector(object item) { if (ItemsSource != null) { - var obs = new ExpressionObserver(item, ItemsSource.Path); + var obs = ExpressionObserverBuilder.Build(item, ItemsSource.Path); return InstancedBinding.OneWay(obs, BindingPriority.Style); } diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 96fc2986e8a..48f74f4d458 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -10,6 +10,7 @@ using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.LogicalTree; +using Avalonia.Markup.Parsers; using Avalonia.VisualTree; namespace Avalonia.Data @@ -86,6 +87,11 @@ public Binding(string path, BindingMode mode = BindingMode.Default) public WeakReference DefaultAnchor { get; set; } + /// + /// Gets or sets a function used to resolve types from names in the binding path. + /// + public Func TypeResolver { get; set; } + /// public InstancedBinding Initiate( IAvaloniaObject target, @@ -193,20 +199,22 @@ private ExpressionObserver CreateDataContextObserver( var update = target.GetObservable(StyledElement.DataContextProperty) .Skip(1) .Select(_ => Unit.Default); - var result = new ExpressionObserver( + var result = ExpressionObserverBuilder.Build( () => target.GetValue(StyledElement.DataContextProperty), path, update, - enableDataValidation); + enableDataValidation, + typeResolver: TypeResolver); return result; } else { - return new ExpressionObserver( + return ExpressionObserverBuilder.Build( GetParentDataContext(target), path, - enableDataValidation); + enableDataValidation, + typeResolver: TypeResolver); } } @@ -219,11 +227,12 @@ private ExpressionObserver CreateElementObserver( Contract.Requires(target != null); var description = $"#{elementName}.{path}"; - var result = new ExpressionObserver( + var result = ExpressionObserverBuilder.Build( ControlLocator.Track(target, elementName), path, enableDataValidation, - description); + description, + typeResolver: TypeResolver); return result; } @@ -255,10 +264,11 @@ private ExpressionObserver CreateFindAncestorObserver( throw new InvalidOperationException("Invalid tree to traverse."); } - return new ExpressionObserver( + return ExpressionObserverBuilder.Build( controlLocator, path, - enableDataValidation); + enableDataValidation, + typeResolver: TypeResolver); } private ExpressionObserver CreateSourceObserver( @@ -268,7 +278,7 @@ private ExpressionObserver CreateSourceObserver( { Contract.Requires(source != null); - return new ExpressionObserver(source, path, enableDataValidation); + return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver); } private ExpressionObserver CreateTemplatedParentObserver( @@ -282,11 +292,12 @@ private ExpressionObserver CreateTemplatedParentObserver( .Skip(1) .Select(_ => Unit.Default); - var result = new ExpressionObserver( + var result = ExpressionObserverBuilder.Build( () => target.GetValue(StyledElement.TemplatedParentProperty), path, update, - enableDataValidation); + enableDataValidation, + typeResolver: TypeResolver); return result; } diff --git a/src/Avalonia.Base/Data/Core/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs similarity index 96% rename from src/Avalonia.Base/Data/Core/Parsers/ArgumentListParser.cs rename to src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index 17200a62b1a..ae48657c013 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -1,11 +1,12 @@ // 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 Avalonia.Data.Core; using System; using System.Collections.Generic; using System.Text; -namespace Avalonia.Data.Core.Parsers +namespace Avalonia.Markup.Parsers { internal static class ArgumentListParser { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs new file mode 100644 index 00000000000..7141a62cd91 --- /dev/null +++ b/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 class ExpressionObserverBuilder + { + internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func 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 typeResolver = null) + { + return new ExpressionObserver( + root, + Parse(expression, enableDataValidation, typeResolver), + description ?? expression); + } + + public static ExpressionObserver Build( + IObservable rootObservable, + string expression, + bool enableDataValidation = false, + string description = null, + Func typeResolver = null) + { + Contract.Requires(rootObservable != null); + return new ExpressionObserver( + rootObservable, + Parse(expression, enableDataValidation, typeResolver), + description ?? expression); + } + + + public static ExpressionObserver Build( + Func rootGetter, + string expression, + IObservable update, + bool enableDataValidation = false, + string description = null, + Func typeResolver = null) + { + Contract.Requires(rootGetter != null); + + return new ExpressionObserver( + () => rootGetter(), + Parse(expression, enableDataValidation, typeResolver), + update, + description ?? expression); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs similarity index 84% rename from src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs rename to src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 5c74c5cd130..6919eeeb0dd 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -1,18 +1,22 @@ // 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 Avalonia.Data.Core; +using Avalonia.Markup.Parsers.Nodes; using System; using System.Collections.Generic; using System.Linq; -namespace Avalonia.Data.Core.Parsers +namespace Avalonia.Markup.Parsers { internal class ExpressionParser { - private bool _enableValidation; + private readonly bool _enableValidation; + private readonly Func _typeResolver; - public ExpressionParser(bool enableValidation) + public ExpressionParser(bool enableValidation, Func typeResolver) { + _typeResolver = typeResolver; _enableValidation = enableValidation; } @@ -130,7 +134,19 @@ private State ParseBeforeMember(Reader r, IList nodes) private State ParseAttachedProperty(Reader r, List 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('.')) { @@ -144,7 +160,9 @@ private State ParseAttachedProperty(Reader r, List nodes) throw new ExpressionParseException(r.Position, "Expected ')'."); } - nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation)); + var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns, owner), name); + + nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation)); return State.AfterMember; } @@ -157,7 +175,7 @@ private State ParseIndexer(Reader r, List nodes) throw new ExpressionParseException(r.Position, "Indexer may not be empty."); } - nodes.Add(new IndexerNode(args)); + nodes.Add(new StringIndexerNode(args)); return State.AfterMember; } diff --git a/src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs similarity index 97% rename from src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs rename to src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs index b0a9ff4df23..f86f2db3216 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs @@ -4,7 +4,7 @@ using System.Globalization; using System.Text; -namespace Avalonia.Data.Core.Parsers +namespace Avalonia.Markup.Parsers { internal static class IdentifierParser { diff --git a/src/Avalonia.Base/Data/Core/IndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs similarity index 98% rename from src/Avalonia.Base/Data/Core/IndexerNode.cs rename to src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index 251b47c24c8..01eb0a9b53b 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -12,12 +12,13 @@ using System.Reflection; using System.Reactive.Linq; using Avalonia.Data; +using Avalonia.Data.Core; -namespace Avalonia.Data.Core +namespace Avalonia.Markup.Parsers.Nodes { - internal class IndexerNode : IndexerNodeBase + internal class StringIndexerNode : IndexerNodeBase { - public IndexerNode(IList arguments) + public StringIndexerNode(IList arguments) { Arguments = arguments; } diff --git a/src/Avalonia.Base/Data/Core/Parsers/Reader.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs similarity index 96% rename from src/Avalonia.Base/Data/Core/Parsers/Reader.cs rename to src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs index 14187c769a9..9355bc9aa31 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/Reader.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs @@ -3,7 +3,7 @@ using System; -namespace Avalonia.Data.Core.Parsers +namespace Avalonia.Markup.Parsers { internal class Reader { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index e50056ddef9..bb76387e615 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -52,11 +52,11 @@ public Selector Parse(string s) if (ofType != null) { - result = result.OfType(_typeResolver(ofType.TypeName, ofType.Xmlns)); + result = result.OfType(_typeResolver(ofType.Xmlns, ofType.TypeName)); } if (@is != null) { - result = result.Is(_typeResolver(@is.TypeName, @is.Xmlns)); + result = result.Is(_typeResolver(@is.Xmlns, @is.TypeName)); } else if (@class != null) { diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index 6b71d28e22e..4e595684df0 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -10,6 +10,7 @@ using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Moq; using Xunit; @@ -22,7 +23,7 @@ public class BindingExpressionTests : IClassFixture public async Task Should_Get_Simple_Property_Value() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(string)); var result = await target.Take(1); Assert.Equal("foo", result); @@ -34,7 +35,7 @@ public async Task Should_Get_Simple_Property_Value() public void Should_Set_Simple_Property_Value() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(string)); target.OnNext("bar"); @@ -47,7 +48,7 @@ public void Should_Set_Simple_Property_Value() public void Should_Set_Indexed_Value() { var data = new { Foo = new[] { "foo" } }; - var target = new BindingExpression(new ExpressionObserver(data, "Foo[0]"), typeof(string)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "Foo[0]"), typeof(string)); target.OnNext("bar"); @@ -60,7 +61,7 @@ public void Should_Set_Indexed_Value() public async Task Should_Convert_Get_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); var result = await target.Take(1); Assert.Equal(5.6, result); @@ -72,7 +73,7 @@ public async Task Should_Convert_Get_String_To_Double() public async Task Getting_Invalid_Double_String_Should_Return_BindingError() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); var result = await target.Take(1); Assert.IsType(result); @@ -84,7 +85,7 @@ public async Task Getting_Invalid_Double_String_Should_Return_BindingError() public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() { var data = new Class1 { StringValue = null }; - var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -96,7 +97,7 @@ public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() public void Should_Convert_Set_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); target.OnNext(6.7); @@ -109,7 +110,7 @@ public void Should_Convert_Set_String_To_Double() public async Task Should_Convert_Get_Double_To_String() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); var result = await target.Take(1); Assert.Equal($"{5.6}", result); @@ -121,7 +122,7 @@ public async Task Should_Convert_Get_Double_To_String() public void Should_Convert_Set_Double_To_String() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); target.OnNext($"{6.7}"); @@ -135,7 +136,7 @@ public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonCo { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - new ExpressionObserver(data, "StringValue"), + ExpressionObserverBuilder.Build(data, "StringValue"), typeof(int), 42, DefaultValueConverter.Instance); @@ -156,7 +157,7 @@ public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonCo { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - new ExpressionObserver(data, "StringValue", true), + ExpressionObserverBuilder.Build(data, "StringValue", true), typeof(int), 42, DefaultValueConverter.Instance); @@ -177,7 +178,7 @@ public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue() { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - new ExpressionObserver(data, "StringValue"), + ExpressionObserverBuilder.Build(data, "StringValue"), typeof(int), "bar", DefaultValueConverter.Instance); @@ -199,7 +200,7 @@ public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue_Wi { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - new ExpressionObserver(data, "StringValue", true), + ExpressionObserverBuilder.Build(data, "StringValue", true), typeof(int), "bar", DefaultValueConverter.Instance); @@ -220,7 +221,7 @@ public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue_Wi public void Setting_Invalid_Double_String_Should_Not_Change_Target() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); target.OnNext("foo"); @@ -234,7 +235,7 @@ public void Setting_Invalid_Double_String_Should_Use_FallbackValue() { var data = new Class1 { DoubleValue = 5.6 }; var target = new BindingExpression( - new ExpressionObserver(data, "DoubleValue"), + ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string), "9.8", DefaultValueConverter.Instance); @@ -250,7 +251,7 @@ public void Setting_Invalid_Double_String_Should_Use_FallbackValue() public void Should_Coerce_Setting_Null_Double_To_Default_Value() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); target.OnNext(null); @@ -263,7 +264,7 @@ public void Should_Coerce_Setting_Null_Double_To_Default_Value() public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); target.OnNext(AvaloniaProperty.UnsetValue); @@ -279,7 +280,7 @@ public void Should_Pass_ConverterParameter_To_Convert() var converter = new Mock(); var target = new BindingExpression( - new ExpressionObserver(data, "DoubleValue"), + ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string), converter.Object, converterParameter: "foo"); @@ -297,7 +298,7 @@ public void Should_Pass_ConverterParameter_To_ConvertBack() var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); var target = new BindingExpression( - new ExpressionObserver(data, "DoubleValue"), + ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string), converter.Object, converterParameter: "foo"); @@ -314,7 +315,7 @@ public void Should_Handle_DataValidation() { var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string)); + var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue", true), typeof(string)); var result = new List(); target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs index 3ed2c0b7eb9..112a7fc4d0a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs @@ -8,21 +8,25 @@ using Avalonia.Diagnostics; using Avalonia.Data.Core; using Xunit; +using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { public class ExpressionObserverTests_AttachedProperty { + private readonly Func _typeResolver; + public ExpressionObserverTests_AttachedProperty() { var foo = Owner.FooProperty; + _typeResolver = (_, name) => name == "Owner" ? typeof(Owner) : null; } [Fact] public async Task Should_Get_Attached_Property_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "(Owner.Foo)"); + var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver); var result = await target.Take(1); Assert.Equal("foo", result); @@ -30,6 +34,19 @@ public async Task Should_Get_Attached_Property_Value() 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() { @@ -41,7 +58,7 @@ public async Task Should_Get_Chained_Attached_Property_Value() } }; - var target = new ExpressionObserver(data, "Next.(Owner.Foo)"); + var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver); var result = await target.Take(1); Assert.Equal("bar", result); @@ -53,7 +70,7 @@ public async Task Should_Get_Chained_Attached_Property_Value() public void Should_Track_Simple_Attached_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "(Owner.Foo)"); + var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -77,7 +94,7 @@ public void Should_Track_Chained_Attached_Value() } }; - var target = new ExpressionObserver(data, "Next.(Owner.Foo)"); + var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -96,7 +113,7 @@ public void Should_Not_Keep_Source_Alive() Func> run = () => { var source = new Class1(); - var target = new ExpressionObserver(source, "(Owner.Foo)"); + var target = ExpressionObserverBuilder.Build(source, "(Owner.Foo)", typeResolver: _typeResolver); return Tuple.Create(target, new WeakReference(source)); }; @@ -113,7 +130,7 @@ public void Should_Fail_With_Attached_Property_With_Only_1_Part() { var data = new Class1(); - Assert.Throws(() => new ExpressionObserver(data, "(Owner)")); + Assert.Throws(() => ExpressionObserverBuilder.Build(data, "(Owner)", typeResolver: _typeResolver)); } [Fact] @@ -121,7 +138,7 @@ public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts() { var data = new Class1(); - Assert.Throws(() => new ExpressionObserver(data, "(Owner.Foo.Bar)")); + Assert.Throws(() => ExpressionObserverBuilder.Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver)); } private static class Owner diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs index bf2b6cbcb22..b34c1ff8be7 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs @@ -8,6 +8,7 @@ using Avalonia.Diagnostics; using Avalonia.Data.Core; using Xunit; +using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { @@ -22,7 +23,7 @@ public ExpressionObserverTests_AvaloniaProperty() public async Task Should_Get_Simple_Property_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result = await target.Take(1); Assert.Equal("foo", result); @@ -34,7 +35,7 @@ public async Task Should_Get_Simple_Property_Value() public async Task Should_Get_Simple_ClrProperty_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "ClrProperty"); + var target = ExpressionObserverBuilder.Build(data, "ClrProperty"); var result = await target.Take(1); Assert.Equal("clr-property", result); @@ -44,7 +45,7 @@ public async Task Should_Get_Simple_ClrProperty_Value() public void Should_Track_Simple_Property_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -63,7 +64,7 @@ public void Should_Not_Keep_Source_Alive() Func> run = () => { var source = new Class1(); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserverBuilder.Build(source, "Foo"); return Tuple.Create(target, new WeakReference(source)); }; diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index 37325697531..c5f7d1d7bf8 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Xunit; @@ -19,7 +20,7 @@ public class ExpressionObserverTests_DataValidation : IClassFixture() @@ -36,7 +37,7 @@ public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() public void Exception_Validation_Sends_DataValidationError() { var data = new ExceptionTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true); var validationMessageFound = false; observer.OfType() @@ -53,7 +54,7 @@ public void Exception_Validation_Sends_DataValidationError() public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); + var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), false); observer.Subscribe(_ => { }); @@ -64,7 +65,7 @@ public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enable public void Enabled_Indei_Validation_Subscribes() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true); var sub = observer.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); @@ -76,7 +77,7 @@ public void Enabled_Indei_Validation_Subscribes() public void Validation_Plugins_Send_Correct_Notifications() { var data = new IndeiTest(); - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true); var result = new List(); var errmsg = string.Empty; @@ -122,7 +123,7 @@ public void Doesnt_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain() Inner = new IndeiTest() }; - var observer = new ExpressionObserver( + var observer = ExpressionObserverBuilder.Build( data, $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", true); @@ -142,7 +143,7 @@ public void Sends_Correct_Notifications_With_Property_Chain() var container = new Container(); var inner = new IndeiTest(); - var observer = new ExpressionObserver( + var observer = ExpressionObserverBuilder.Build( container, $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", true); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs index 8a54f968b15..da167e50088 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs @@ -11,6 +11,7 @@ using Avalonia.Data.Core; using Avalonia.UnitTests; using Xunit; +using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { @@ -20,7 +21,7 @@ public class ExpressionObserverTests_Indexer public async Task Should_Get_Array_Value() { var data = new { Foo = new [] { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); var result = await target.Take(1); Assert.Equal("bar", result); @@ -32,7 +33,7 @@ public async Task Should_Get_Array_Value() 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 target = ExpressionObserverBuilder.Build(data, "Foo[invalid]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -44,7 +45,7 @@ public async Task Should_Get_UnsetValue_For_Invalid_Array_Index() public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index() { var data = new { Foo = new Dictionary { { 1, "foo" } } }; - var target = new ExpressionObserver(data, "Foo[invalid]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -56,7 +57,7 @@ public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index() public async Task Should_Get_UnsetValue_For_Object_Without_Indexer() { var data = new { Foo = 5 }; - var target = new ExpressionObserver(data, "Foo[noindexer]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[noindexer]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -68,7 +69,7 @@ public async Task Should_Get_UnsetValue_For_Object_Without_Indexer() public async Task Should_Get_MultiDimensional_Array_Value() { var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; - var target = new ExpressionObserver(data, "Foo[1, 1]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[1, 1]"); var result = await target.Take(1); Assert.Equal("qux", result); @@ -80,7 +81,7 @@ public async Task Should_Get_MultiDimensional_Array_Value() public async Task Should_Get_Value_For_String_Indexer() { var data = new { Foo = new Dictionary { { "foo", "bar" }, { "baz", "qux" } } }; - var target = new ExpressionObserver(data, "Foo[foo]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); var result = await target.Take(1); Assert.Equal("bar", result); @@ -92,7 +93,7 @@ public async Task Should_Get_Value_For_String_Indexer() public async Task Should_Get_Value_For_Non_String_Indexer() { var data = new { Foo = new Dictionary { { 1.0, "bar" }, { 2.0, "qux" } } }; - var target = new ExpressionObserver(data, "Foo[1.0]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[1.0]"); var result = await target.Take(1); Assert.Equal("bar", result); @@ -104,7 +105,7 @@ public async Task Should_Get_Value_For_Non_String_Indexer() public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[2]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -116,7 +117,7 @@ public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() 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 = ExpressionObserverBuilder.Build(data, "Foo[1,2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -128,7 +129,7 @@ public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue() public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new List { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[2]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -140,7 +141,7 @@ public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() public async Task Should_Get_List_Value() { var data = new { Foo = new List { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); var result = await target.Take(1); Assert.Equal("bar", result); @@ -152,7 +153,7 @@ public async Task Should_Get_List_Value() public void Should_Track_INCC_Add() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[2]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -170,7 +171,7 @@ public void Should_Track_INCC_Add() public void Should_Track_INCC_Remove() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[0]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[0]"); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -188,7 +189,7 @@ public void Should_Track_INCC_Remove() public void Should_Track_INCC_Replace() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -209,7 +210,7 @@ public void Should_Track_INCC_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 { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -225,7 +226,7 @@ public void Should_Track_INCC_Move() public void Should_Track_INCC_Reset() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -244,7 +245,7 @@ public void Should_Track_NonIntegerIndexer() data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = new ExpressionObserver(data, "Foo[foo]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -263,7 +264,7 @@ public void Should_Track_NonIntegerIndexer() public void Should_SetArrayIndex() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); using (target.Subscribe(_ => { })) { @@ -286,7 +287,7 @@ public void Should_Set_ExistingDictionaryEntry() } }; - var target = new ExpressionObserver(data, "Foo[foo]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); using (target.Subscribe(_ => { })) { Assert.True(target.SetValue(4)); @@ -308,7 +309,7 @@ public void Should_Add_NewDictionaryEntry() } }; - var target = new ExpressionObserver(data, "Foo[bar]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[bar]"); using (target.Subscribe(_ => { })) { Assert.True(target.SetValue(4)); @@ -326,7 +327,7 @@ public void Should_Set_NonIntegerIndexer() data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = new ExpressionObserver(data, "Foo[foo]"); + var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); using (target.Subscribe(_ => { })) { @@ -343,7 +344,7 @@ public async Task Indexer_Only_Binding_Works() { var data = new[] { 1, 2, 3 }; - var target = new ExpressionObserver(data, "[1]"); + var target = ExpressionObserverBuilder.Build(data, "[1]"); var value = await target.Take(1); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs index b88bf2c4275..4f3980e59a8 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs @@ -9,6 +9,7 @@ using Microsoft.Reactive.Testing; using Avalonia.Data.Core; using Xunit; +using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { @@ -18,7 +19,7 @@ public class ExpressionObserverTests_Lifetime public void Should_Complete_When_Source_Observable_Completes() { var source = new BehaviorSubject(1); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserverBuilder.Build(source, "Foo"); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -31,7 +32,7 @@ public void Should_Complete_When_Source_Observable_Completes() public void Should_Complete_When_Source_Observable_Errors() { var source = new BehaviorSubject(1); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserverBuilder.Build(source, "Foo"); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -44,7 +45,7 @@ public void Should_Complete_When_Source_Observable_Errors() public void Should_Complete_When_Update_Observable_Completes() { var update = new Subject(); - var target = new ExpressionObserver(() => 1, "Foo", update); + var target = ExpressionObserverBuilder.Build(() => 1, "Foo", update); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -57,7 +58,7 @@ public void Should_Complete_When_Update_Observable_Completes() public void Should_Complete_When_Update_Observable_Errors() { var update = new Subject(); - var target = new ExpressionObserver(() => 1, "Foo", update); + var target = ExpressionObserverBuilder.Build(() => 1, "Foo", update); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -72,7 +73,7 @@ public void Should_Unsubscribe_From_Source_Observable() var scheduler = new TestScheduler(); var source = scheduler.CreateColdObservable( OnNext(1, new { Foo = "foo" })); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserverBuilder.Build(source, "Foo"); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -91,7 +92,7 @@ public void Should_Unsubscribe_From_Update_Observable() var scheduler = new TestScheduler(); var update = scheduler.CreateColdObservable(); var data = new { Foo = "foo" }; - var target = new ExpressionObserver(() => data, "Foo", update); + var target = ExpressionObserverBuilder.Build(() => data, "Foo", update); var result = new List(); using (target.Subscribe(x => result.Add(x))) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs index ef89c2b4bdd..6bb448158e8 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs @@ -1,5 +1,6 @@ using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using System; using System.Collections.Generic; using System.Linq; @@ -30,7 +31,7 @@ public static void TooManyParameters(int a1, int a2, int a3, int a4, int a5, int public async Task Should_Get_Method() { 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); Assert.NotNull(result); @@ -46,7 +47,7 @@ public async Task Should_Get_Method() public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, Type expectedType) { var data = new TestObject(); - var observer = new ExpressionObserver(data, methodName); + var observer = ExpressionObserverBuilder.Build(data, methodName); var result = await observer.Take(1); Assert.IsType(expectedType, result); @@ -58,7 +59,7 @@ public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, T public async Task Can_Call_Method_Returned_From_Observer() { 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 callback = (Func)result; @@ -74,7 +75,7 @@ public async Task Can_Call_Method_Returned_From_Observer() public async Task Should_Return_Error_Notification_If_Too_Many_Parameters(string methodName) { var data = new TestObject(); - var observer = new ExpressionObserver(data, methodName); + var observer = ExpressionObserverBuilder.Build(data, methodName); var result = await observer.Take(1); Assert.IsType(result); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs index 556352f6ca3..9ca1d07ee0e 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Xunit; namespace Avalonia.Base.UnitTests.Data.Core @@ -16,7 +17,7 @@ public class ExpressionObserverTests_Negation public async Task Should_Negate_Boolean_Value() { var data = new { Foo = true }; - var target = new ExpressionObserver(data, "!Foo"); + var target = ExpressionObserverBuilder.Build(data, "!Foo"); var result = await target.Take(1); Assert.False((bool)result); @@ -28,7 +29,7 @@ public async Task Should_Negate_Boolean_Value() public async Task Should_Negate_0() { var data = new { Foo = 0 }; - var target = new ExpressionObserver(data, "!Foo"); + var target = ExpressionObserverBuilder.Build(data, "!Foo"); var result = await target.Take(1); Assert.True((bool)result); @@ -40,7 +41,7 @@ public async Task Should_Negate_0() public async Task Should_Negate_1() { var data = new { Foo = 1 }; - var target = new ExpressionObserver(data, "!Foo"); + var target = ExpressionObserverBuilder.Build(data, "!Foo"); var result = await target.Take(1); Assert.False((bool)result); @@ -52,7 +53,7 @@ public async Task Should_Negate_1() public async Task Should_Negate_False_String() { var data = new { Foo = "false" }; - var target = new ExpressionObserver(data, "!Foo"); + var target = ExpressionObserverBuilder.Build(data, "!Foo"); var result = await target.Take(1); Assert.True((bool)result); @@ -64,7 +65,7 @@ public async Task Should_Negate_False_String() public async Task Should_Negate_True_String() { var data = new { Foo = "True" }; - var target = new ExpressionObserver(data, "!Foo"); + var target = ExpressionObserverBuilder.Build(data, "!Foo"); var result = await target.Take(1); Assert.False((bool)result); @@ -76,7 +77,7 @@ public async Task Should_Negate_True_String() public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() { var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "!Foo"); + var target = ExpressionObserverBuilder.Build(data, "!Foo"); var result = await target.Take(1); Assert.Equal( @@ -92,7 +93,7 @@ public async Task Should_Return_BindingNotification_For_String_Not_Convertible_T 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 target = ExpressionObserverBuilder.Build(data, "!Foo"); var result = await target.Take(1); Assert.Equal( @@ -108,7 +109,7 @@ public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To public void SetValue_Should_Return_False_For_Invalid_Value() { var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "!Foo"); + var target = ExpressionObserverBuilder.Build(data, "!Foo"); target.Subscribe(_ => { }); Assert.False(target.SetValue("bar")); @@ -120,7 +121,7 @@ public void SetValue_Should_Return_False_For_Invalid_Value() public void Can_SetValue_For_Valid_Value() { var data = new Test { Foo = true }; - var target = new ExpressionObserver(data, "!Foo"); + var target = ExpressionObserverBuilder.Build(data, "!Foo"); target.Subscribe(_ => { }); Assert.True(target.SetValue(true)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs index f1c39617eb4..2a8ead4e485 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs @@ -7,6 +7,7 @@ using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Xunit; @@ -21,7 +22,7 @@ public void Should_Not_Get_Observable_Value_Without_Modifier_Char() { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -41,7 +42,7 @@ public void Should_Get_Simple_Observable_Value() { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = new ExpressionObserver(data, "Foo^"); + var target = ExpressionObserverBuilder.Build(data, "Foo^"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -60,7 +61,7 @@ public void Should_Get_Property_Value_From_Observable() using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new Class1(); - var target = new ExpressionObserver(data, "Next^.Foo"); + var target = ExpressionObserverBuilder.Build(data, "Next^.Foo"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -83,7 +84,7 @@ public void Should_Get_Simple_Observable_Value_With_DataValidation_Enabled() { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = new ExpressionObserver(data, "Foo^", true); + var target = ExpressionObserverBuilder.Build(data, "Foo^", true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -105,7 +106,7 @@ public void Should_Get_Property_Value_From_Observable_With_DataValidation_Enable { var data1 = new Class1(); var data2 = new Class2("foo"); - var target = new ExpressionObserver(data1, "Next^.Foo", true); + var target = ExpressionObserverBuilder.Build(data1, "Next^.Foo", true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -128,7 +129,7 @@ public void Should_Return_BindingNotification_If_Stream_Operator_Applied_To_Not_ using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new Class2("foo"); - var target = new ExpressionObserver(data, "Foo^", true); + var target = ExpressionObserverBuilder.Build(data, "Foo^", true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index a3cb11114aa..9381d073222 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -12,6 +12,7 @@ using Avalonia.UnitTests; using Xunit; using System.Threading.Tasks; +using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { @@ -21,7 +22,7 @@ public class ExpressionObserverTests_Property public async Task Should_Get_Simple_Property_Value() { var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result = await target.Take(1); Assert.Equal("foo", result); @@ -33,7 +34,7 @@ public async Task Should_Get_Simple_Property_Value() public void Should_Get_Simple_Property_Value_Type() { var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); target.Subscribe(_ => { }); @@ -46,7 +47,7 @@ public void Should_Get_Simple_Property_Value_Type() public async Task Should_Get_Simple_Property_Value_Null() { var data = new { Foo = (string)null }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result = await target.Take(1); Assert.Null(result); @@ -58,7 +59,7 @@ public async Task Should_Get_Simple_Property_Value_Null() public async Task Should_Get_Simple_Property_From_Base_Class() { var data = new Class3 { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result = await target.Take(1); Assert.Equal("foo", result); @@ -70,7 +71,7 @@ public async Task Should_Get_Simple_Property_From_Base_Class() 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 = ExpressionObserverBuilder.Build(default(object), "Foo"); var result = await target.Take(1); Assert.Equal( @@ -87,7 +88,7 @@ public async Task Should_Return_BindingNotification_Error_For_Root_Null() 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 = ExpressionObserverBuilder.Build(AvaloniaProperty.UnsetValue, "Foo"); var result = await target.Take(1); Assert.Equal( @@ -104,7 +105,7 @@ public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue() 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 = ExpressionObserverBuilder.Build(Observable.Return(default(object)), "Foo"); var result = await target.Take(1); Assert.Equal( @@ -121,7 +122,7 @@ public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Nu 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 = ExpressionObserverBuilder.Build(Observable.Return(AvaloniaProperty.UnsetValue), "Foo"); var result = await target.Take(1); Assert.Equal( @@ -138,7 +139,7 @@ public async void Should_Return_BindingNotification_Error_For_Observable_Root_Un public async Task Should_Get_Simple_Property_Chain() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; - var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); var result = await target.Take(1); Assert.Equal("baz", result); @@ -150,7 +151,7 @@ public async Task Should_Get_Simple_Property_Chain() public void Should_Get_Simple_Property_Chain_Type() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; - var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); target.Subscribe(_ => { }); @@ -163,7 +164,7 @@ public void Should_Get_Simple_Property_Chain_Type() 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 target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); var result = await target.Take(1); Assert.IsType(result); @@ -180,7 +181,7 @@ public async Task Should_Return_BindingNotification_Error_For_Broken_Chain() 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 target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -202,7 +203,7 @@ public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value() public void Should_Have_Null_ResultType_For_Broken_Chain() { var data = new { Foo = new { Bar = 1 } }; - var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); Assert.Null(target.ResultType); @@ -213,7 +214,7 @@ public void Should_Have_Null_ResultType_For_Broken_Chain() public void Should_Track_Simple_Property_Value() { var data = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -232,7 +233,7 @@ public void Should_Track_Simple_Property_Value() public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String() { var data = new Class1 { Bar = "foo" }; - var target = new ExpressionObserver(data, "Bar"); + var target = ExpressionObserverBuilder.Build(data, "Bar"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -262,7 +263,7 @@ public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String() public void Should_Track_End_Of_Property_Chain_Changing() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -283,7 +284,7 @@ public void Should_Track_End_Of_Property_Chain_Changing() public void Should_Track_Property_Chain_Changing() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -316,7 +317,7 @@ public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() } }; - var target = new ExpressionObserver(data, "Next.Next.Bar"); + var target = ExpressionObserverBuilder.Build(data, "Next.Next.Bar"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -349,7 +350,7 @@ public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -384,7 +385,7 @@ public void Empty_Expression_Should_Track_Root() { var data = new Class1 { Foo = "foo" }; var update = new Subject(); - var target = new ExpressionObserver(() => data.Foo, "", update); + var target = ExpressionObserverBuilder.Build(() => data.Foo, "", update); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -404,7 +405,7 @@ public void Should_Track_Property_Value_From_Observable_Root() var source = scheduler.CreateColdObservable( OnNext(1, new Class1 { Foo = "foo" }), OnNext(2, new Class1 { Foo = "bar" })); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserverBuilder.Build(source, "Foo"); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -420,7 +421,7 @@ public void Should_Track_Property_Value_From_Observable_Root() public void Subscribing_Multiple_Times_Should_Return_Values_To_All() { var data = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result1 = new List(); var result2 = new List(); var result3 = new List(); @@ -443,7 +444,7 @@ public void Subscribing_Multiple_Times_Should_Return_Values_To_All() public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once() { var data = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var sub1 = target.Subscribe(x => { }); var sub2 = target.Subscribe(x => { }); @@ -462,7 +463,7 @@ public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_ public void SetValue_Should_Set_Simple_Property_Value() { var data = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); using (target.Subscribe(_ => { })) { @@ -478,7 +479,7 @@ public void SetValue_Should_Set_Simple_Property_Value() public void SetValue_Should_Set_Property_At_The_End_Of_Chain() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); using (target.Subscribe(_ => { })) { @@ -494,7 +495,7 @@ public void SetValue_Should_Set_Property_At_The_End_Of_Chain() public void SetValue_Should_Return_False_For_Missing_Property() { var data = new Class1 { Next = new WithoutBar() }; - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); using (target.Subscribe(_ => { })) { @@ -508,7 +509,7 @@ public void SetValue_Should_Return_False_For_Missing_Property() public void SetValue_Should_Notify_New_Value_With_Inpc() { var data = new Class1(); - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -523,7 +524,7 @@ public void SetValue_Should_Notify_New_Value_With_Inpc() public void SetValue_Should_Notify_New_Value_Without_Inpc() { var data = new Class1(); - var target = new ExpressionObserver(data, "Bar"); + var target = ExpressionObserverBuilder.Build(data, "Bar"); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -538,7 +539,7 @@ public void SetValue_Should_Notify_New_Value_Without_Inpc() public void SetValue_Should_Return_False_For_Missing_Object() { var data = new Class1(); - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); using (target.Subscribe(_ => { })) { @@ -555,7 +556,7 @@ public void Can_Replace_Root() var second = new Class1 { Foo = "bar" }; var root = first; var update = new Subject(); - var target = new ExpressionObserver(() => root, "Foo", update); + var target = ExpressionObserverBuilder.Build(() => root, "Foo", update); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -589,7 +590,7 @@ public void Should_Not_Keep_Source_Alive() Func> run = () => { var source = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserverBuilder.Build(source, "Foo"); return Tuple.Create(target, new WeakReference(source)); }; diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs index a163229e268..974ac771555 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs @@ -5,6 +5,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Xunit; @@ -16,7 +17,7 @@ public class ExpressionObserverTests_SetValue public void Should_Set_Simple_Property_Value() { var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); using (target.Subscribe(_ => { })) { @@ -30,7 +31,7 @@ public void Should_Set_Simple_Property_Value() public void Should_Set_Value_On_Simple_Property_Chain() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Foo.Bar"); + var target = ExpressionObserverBuilder.Build(data, "Foo.Bar"); using (target.Subscribe(_ => { })) { @@ -44,7 +45,7 @@ public void Should_Set_Value_On_Simple_Property_Chain() public void Should_Not_Try_To_Set_Value_On_Broken_Chain() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Foo.Bar"); + var target = ExpressionObserverBuilder.Build(data, "Foo.Bar"); // Ensure the ExpressionObserver's subscriptions are kept active. target.OfType().Subscribe(x => { }); @@ -67,7 +68,7 @@ public void Pushing_Null_To_RootObservable_Updates_Leaf_Node() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; var rootObservable = new BehaviorSubject(data); - var target = new ExpressionObserver(rootObservable, "Foo.Bar"); + var target = ExpressionObserverBuilder.Build(rootObservable, "Foo.Bar"); target.Subscribe(_ => { }); rootObservable.OnNext(null); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs index 3b9a23f8466..69dec53d0e6 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Xunit; @@ -21,7 +22,7 @@ public void Should_Not_Get_Task_Result_Without_Modifier_Char() { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserverBuilder.Build(data, "Foo"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -41,7 +42,7 @@ public void Should_Get_Completed_Task_Value() using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = Task.FromResult("foo") }; - var target = new ExpressionObserver(data, "Foo^"); + var target = ExpressionObserverBuilder.Build(data, "Foo^"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -59,7 +60,7 @@ public void Should_Get_Property_Value_From_Task() { var tcs = new TaskCompletionSource(); var data = new Class1(tcs.Task); - var target = new ExpressionObserver(data, "Next^.Foo"); + var target = ExpressionObserverBuilder.Build(data, "Next^.Foo"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -79,7 +80,7 @@ public void Should_Return_BindingNotification_Error_On_Task_Exception() { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = new ExpressionObserver(data, "Foo^"); + var target = ExpressionObserverBuilder.Build(data, "Foo^"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -105,7 +106,7 @@ public void Should_Return_BindingNotification_Error_For_Faulted_Task() using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = TaskFromException(new NotSupportedException()) }; - var target = new ExpressionObserver(data, "Foo^"); + var target = ExpressionObserverBuilder.Build(data, "Foo^"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -130,7 +131,7 @@ public void Should_Get_Simple_Task_Value_With_Data_DataValidation_Enabled() { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = new ExpressionObserver(data, "Foo^", true); + var target = ExpressionObserverBuilder.Build(data, "Foo^", true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index a7263cacbd1..0bb2de637a0 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -515,7 +515,7 @@ public IControl Build(object param) public InstancedBinding ItemsSelector(object item) { - var obs = new ExpressionObserver(item, nameof(Node.Children)); + var obs = ExpressionObserver.CreateFromExpression(item, o => (o as Node).Children); return InstancedBinding.OneWay(obs); } diff --git a/tests/Avalonia.LeakTests/ExpressionObserverTests.cs b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs index 96f9e37897b..e59472dde65 100644 --- a/tests/Avalonia.LeakTests/ExpressionObserverTests.cs +++ b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs @@ -24,7 +24,7 @@ public void Should_Not_Keep_Source_Alive_ObservableCollection() Func run = () => { var source = new { Foo = new AvaloniaList {"foo", "bar"} }; - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.CreateFromExpression(source, o => o.Foo); target.Subscribe(_ => { }); return target; @@ -42,7 +42,7 @@ public void Should_Not_Keep_Source_Alive_ObservableCollection_With_DataValidatio Func run = () => { var source = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(source, "Foo", true); + var target = ExpressionObserver.CreateFromExpression(source, o => o.Foo, true); target.Subscribe(_ => { }); return target; @@ -60,7 +60,7 @@ public void Should_Not_Keep_Source_Alive_NonIntegerIndexer() Func run = () => { var source = new { Foo = new NonIntegerIndexer() }; - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.CreateFromExpression(source, o => o.Foo); target.Subscribe(_ => { }); return target; @@ -78,7 +78,7 @@ public void Should_Not_Keep_Source_Alive_MethodBinding() Func run = () => { var source = new { Foo = new MethodBound() }; - var target = new ExpressionObserver(source, "Foo.A"); + var target = ExpressionObserver.CreateFromExpression(source, o => (Action)o.Foo.A); target.Subscribe(_ => { }); return target; }; diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs similarity index 75% rename from tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests.cs rename to tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs index 146b7cace13..8bb23815528 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs @@ -4,16 +4,18 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Parsers.Nodes; using Xunit; -namespace Avalonia.Base.UnitTests.Data.Core +namespace Avalonia.Markup.UnitTests.Parsers { - public class ExpressionNodeBuilderTests + public class ExpressionObserverBuilderTests { [Fact] public void Should_Build_Single_Property() { - var result = ToList(ExpressionNodeBuilder.Build("Foo")); + var result = ToList(ExpressionObserverBuilder.Parse("Foo")); AssertIsProperty(result[0], "Foo"); } @@ -21,7 +23,7 @@ public void Should_Build_Single_Property() [Fact] public void Should_Build_Underscored_Property() { - var result = ToList(ExpressionNodeBuilder.Build("_Foo")); + var result = ToList(ExpressionObserverBuilder.Parse("_Foo")); AssertIsProperty(result[0], "_Foo"); } @@ -29,7 +31,7 @@ public void Should_Build_Underscored_Property() [Fact] public void Should_Build_Property_With_Digits() { - var result = ToList(ExpressionNodeBuilder.Build("F0o")); + var result = ToList(ExpressionObserverBuilder.Parse("F0o")); AssertIsProperty(result[0], "F0o"); } @@ -37,7 +39,7 @@ public void Should_Build_Property_With_Digits() [Fact] 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); AssertIsProperty(result[0], "Foo"); @@ -48,7 +50,7 @@ public void Should_Build_Property_Chain() [Fact] 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.IsType(result[0]); @@ -60,7 +62,7 @@ public void Should_Build_Negated_Property_Chain() [Fact] 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.IsType(result[0]); @@ -73,29 +75,29 @@ public void Should_Build_Double_Negated_Property_Chain() [Fact] 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); AssertIsProperty(result[0], "Foo"); AssertIsIndexer(result[1], "15"); - Assert.IsType(result[1]); + Assert.IsType(result[1]); } [Fact] 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); AssertIsProperty(result[0], "Foo"); AssertIsIndexer(result[1], "Key"); - Assert.IsType(result[1]); + Assert.IsType(result[1]); } [Fact] 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); AssertIsProperty(result[0], "Foo"); @@ -105,7 +107,7 @@ public void Should_Build_Multiple_Indexed_Property() [Fact] 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); AssertIsProperty(result[0], "Foo"); @@ -115,7 +117,7 @@ public void Should_Build_Multiple_Indexed_Property_With_Space() [Fact] 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); AssertIsProperty(result[0], "Foo"); @@ -126,7 +128,7 @@ public void Should_Build_Consecutive_Indexers() [Fact] 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); AssertIsProperty(result[0], "Foo"); @@ -145,9 +147,9 @@ private void AssertIsProperty(ExpressionNode node, string name) private void AssertIsIndexer(ExpressionNode node, params string[] args) { - Assert.IsType(node); + Assert.IsType(node); - var e = (IndexerNode)node; + var e = (StringIndexerNode)node; Assert.Equal(e.Arguments.ToArray(), args); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests_Errors.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs similarity index 67% rename from tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests_Errors.cs rename to tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs index 1bf1ce132a0..347fc0a744f 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests_Errors.cs +++ b/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. using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Xunit; -namespace Avalonia.Base.UnitTests.Data.Core +namespace Avalonia.Markup.UnitTests.Parsers { - public class ExpressionNodeBuilderTests_Errors + public class ExpressionObserverBuilderTests_Errors { [Fact] public void Identifier_Cannot_Start_With_Digit() { Assert.Throws( - () => ExpressionNodeBuilder.Build("1Foo")); + () => ExpressionObserverBuilder.Parse("1Foo")); } [Fact] public void Identifier_Cannot_Start_With_Symbol() { Assert.Throws( - () => ExpressionNodeBuilder.Build("Foo.%Bar")); + () => ExpressionObserverBuilder.Parse("Foo.%Bar")); } [Fact] public void Expression_Cannot_End_With_Period() { Assert.Throws( - () => ExpressionNodeBuilder.Build("Foo.Bar.")); + () => ExpressionObserverBuilder.Parse("Foo.Bar.")); } [Fact] public void Expression_Cannot_Have_Empty_Indexer() { Assert.Throws( - () => ExpressionNodeBuilder.Build("Foo.Bar[]")); + () => ExpressionObserverBuilder.Parse("Foo.Bar[]")); } [Fact] public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer() { Assert.Throws( - () => ExpressionNodeBuilder.Build("Foo.Bar[,3,4]")); + () => ExpressionObserverBuilder.Parse("Foo.Bar[,3,4]")); } [Fact] public void Expression_Cannot_Have_Extra_Comma_In_Indexer() { Assert.Throws( - () => ExpressionNodeBuilder.Build("Foo.Bar[3,,4]")); + () => ExpressionObserverBuilder.Parse("Foo.Bar[3,,4]")); } [Fact] public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer() { Assert.Throws( - () => ExpressionNodeBuilder.Build("Foo.Bar[3,4,]")); + () => ExpressionObserverBuilder.Parse("Foo.Bar[3,4,]")); } [Fact] public void Expression_Cannot_Have_Digit_After_Indexer() { Assert.Throws( - () => ExpressionNodeBuilder.Build("Foo.Bar[3,4]5")); + () => ExpressionObserverBuilder.Parse("Foo.Bar[3,4]5")); } [Fact] public void Expression_Cannot_Have_Letter_After_Indexer() { Assert.Throws( - () => ExpressionNodeBuilder.Build("Foo.Bar[3,4]A")); + () => ExpressionObserverBuilder.Parse("Foo.Bar[3,4]A")); } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 8cb2639125f..62a9e80585d 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -6,7 +6,7 @@ using Sprache; using Xunit; -namespace Avalonia.Markup.UnitTest.Parsers +namespace Avalonia.Markup.UnitTests.Parsers { public class SelectorGrammarTests { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs index 360be7f9091..f5a08b6d70c 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs @@ -3,7 +3,7 @@ using Avalonia.Markup.Parsers; using Xunit; -namespace Avalonia.Markup.Xaml.UnitTests.Parsers +namespace Avalonia.Markup.UnitTests.Parsers { public class SelectorParserTests { From c6c51dd36fd18dffaaf3cf9e46359a4b44b23793 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 7 Jun 2018 15:07:31 -0500 Subject: [PATCH 05/12] Add pattern-based support for streaming. Fix bugs in method bindings. --- .../Data/Core/ExpressionObserver.cs | 4 +- .../Parsers/ExpressionVisitorNodeBuilder.cs | 14 +++++++ .../Data/Core/StreamBindingExtensions.cs | 22 ++++++++++ .../ExpressionObserverTests_ExpressionTree.cs | 41 +++++++++++++------ .../TreeViewTests.cs | 2 +- .../ExpressionObserverTests.cs | 8 ++-- 6 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 9a3e2995755..513365ebf43 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -83,7 +83,7 @@ public ExpressionObserver( _root = new WeakReference(root); } - public static ExpressionObserver CreateFromExpression( + public static ExpressionObserver Create( T root, Expression> expression, bool enableDataValidation = false, @@ -113,7 +113,7 @@ public ExpressionObserver( _finished = new Subject(); } - public static ExpressionObserver CreateFromExpression( + public static ExpressionObserver Create( IObservable rootObservable, Expression> expression, bool enableDataValidation = false, diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index 614faf00618..f1f082174a1 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -175,6 +175,20 @@ protected override Expression VisitMethodCall(MethodCallExpression node) return visited; } + 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; + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); } diff --git a/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs new file mode 100644 index 00000000000..907a4ac737a --- /dev/null +++ b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs @@ -0,0 +1,22 @@ +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(this Task @this) + { + throw new InvalidOperationException("This should be used only in a binding expression"); + } + + public static T StreamBinding(this IObservable @this) + { + throw new InvalidOperationException("This should be used only in a binding expression"); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs index ebf3ca2a490..58f3b22abd5 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs @@ -16,7 +16,7 @@ public async Task IdentityExpression_Creates_IdentityObserver() { var target = new object(); - var observer = ExpressionObserver.CreateFromExpression(target, o => o); + var observer = ExpressionObserver.Create(target, o => o); Assert.Equal(target, await observer.Take(1)); GC.KeepAlive(target); @@ -27,7 +27,7 @@ public async Task Property_Access_Expression_Observes_Property() { var target = new Class1(); - var observer = ExpressionObserver.CreateFromExpression(target, o => o.Foo); + var observer = ExpressionObserver.Create(target, o => o.Foo); Assert.Null(await observer.Take(1)); @@ -45,7 +45,7 @@ public async Task Property_Access_Expression_Observes_Property() public void Property_Acccess_Expression_Can_Set_Property() { var data = new Class1(); - var target = ExpressionObserver.CreateFromExpression(data, o => o.Foo); + var target = ExpressionObserver.Create(data, o => o.Foo); using (target.Subscribe(_ => { })) { @@ -60,7 +60,7 @@ public async Task Indexer_Accessor_Can_Read_Value() { var data = new[] { 1, 2, 3, 4 }; - var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + var target = ExpressionObserver.Create(data, o => o[0]); Assert.Equal(data[0], await target.Take(1)); GC.KeepAlive(data); @@ -71,7 +71,7 @@ public async Task Indexer_List_Accessor_Can_Read_Value() { var data = new List { 1, 2, 3, 4 }; - var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + var target = ExpressionObserver.Create(data, o => o[0]); Assert.Equal(data[0], await target.Take(1)); GC.KeepAlive(data); @@ -86,7 +86,7 @@ public async Task Indexer_Accessor_Can_Read_Complex_Index() data.Add(key, new object()); - var target = ExpressionObserver.CreateFromExpression(data, o => o[key]); + var target = ExpressionObserver.Create(data, o => o[key]); Assert.Equal(data[key], await target.Take(1)); @@ -98,7 +98,7 @@ public void Indexer_Can_Set_Value() { var data = new[] { 1, 2, 3, 4 }; - var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + var target = ExpressionObserver.Create(data, o => o[0]); using (target.Subscribe(_ => { })) { @@ -113,7 +113,7 @@ public async Task Inheritance_Casts_Should_Be_Ignored() { NotifyingBase test = new Class1 { Foo = "Test" }; - var target = ExpressionObserver.CreateFromExpression(test, o => ((Class1)o).Foo); + var target = ExpressionObserver.Create(test, o => ((Class1)o).Foo); Assert.Equal("Test", await target.Take(1)); @@ -125,7 +125,7 @@ public void Convert_Casts_Should_Error() { var test = 1; - Assert.Throws(() => ExpressionObserver.CreateFromExpression(test, o => (double)o)); + Assert.Throws(() => ExpressionObserver.Create(test, o => (double)o)); } [Fact] @@ -133,7 +133,7 @@ public async Task As_Operator_Should_Be_Ignored() { NotifyingBase test = new Class1 { Foo = "Test" }; - var target = ExpressionObserver.CreateFromExpression(test, o => (o as Class1).Foo); + var target = ExpressionObserver.Create(test, o => (o as Class1).Foo); Assert.Equal("Test", await target.Take(1)); @@ -145,7 +145,7 @@ public async Task Avalonia_Property_Indexer_Reads_Avalonia_Property_Value() { var test = new Class2(); - var target = ExpressionObserver.CreateFromExpression(test, o => o[Class2.FooProperty]); + var target = ExpressionObserver.Create(test, o => o[Class2.FooProperty]); Assert.Equal("foo", await target.Take(1)); @@ -157,13 +157,30 @@ public async Task Complex_Expression_Correctly_Parsed() { var test = new Class1 { Foo = "Test" }; - var target = ExpressionObserver.CreateFromExpression(test, o => o.Foo.Length); + 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(); + + var sub = target.Subscribe(x => result.Add(x)); + + Assert.Equal(new[] { "foo" }, result); + + GC.KeepAlive(data); + } + } + private class Class1 : NotifyingBase { private string _foo; diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 0bb2de637a0..c49c343a459 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -515,7 +515,7 @@ public IControl Build(object param) public InstancedBinding ItemsSelector(object item) { - var obs = ExpressionObserver.CreateFromExpression(item, o => (o as Node).Children); + var obs = ExpressionObserver.Create(item, o => (o as Node).Children); return InstancedBinding.OneWay(obs); } diff --git a/tests/Avalonia.LeakTests/ExpressionObserverTests.cs b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs index e59472dde65..d2276965452 100644 --- a/tests/Avalonia.LeakTests/ExpressionObserverTests.cs +++ b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs @@ -24,7 +24,7 @@ public void Should_Not_Keep_Source_Alive_ObservableCollection() Func run = () => { var source = new { Foo = new AvaloniaList {"foo", "bar"} }; - var target = ExpressionObserver.CreateFromExpression(source, o => o.Foo); + var target = ExpressionObserver.Create(source, o => o.Foo); target.Subscribe(_ => { }); return target; @@ -42,7 +42,7 @@ public void Should_Not_Keep_Source_Alive_ObservableCollection_With_DataValidatio Func run = () => { var source = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = ExpressionObserver.CreateFromExpression(source, o => o.Foo, true); + var target = ExpressionObserver.Create(source, o => o.Foo, true); target.Subscribe(_ => { }); return target; @@ -60,7 +60,7 @@ public void Should_Not_Keep_Source_Alive_NonIntegerIndexer() Func run = () => { var source = new { Foo = new NonIntegerIndexer() }; - var target = ExpressionObserver.CreateFromExpression(source, o => o.Foo); + var target = ExpressionObserver.Create(source, o => o.Foo); target.Subscribe(_ => { }); return target; @@ -78,7 +78,7 @@ public void Should_Not_Keep_Source_Alive_MethodBinding() Func run = () => { var source = new { Foo = new MethodBound() }; - var target = ExpressionObserver.CreateFromExpression(source, o => (Action)o.Foo.A); + var target = ExpressionObserver.Create(source, o => (Action)o.Foo.A); target.Subscribe(_ => { }); return target; }; From 8a6be395b5d7d91025da47bb66641ac2ca2bb717 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 7 Jun 2018 15:19:24 -0500 Subject: [PATCH 06/12] Remove now-unneeded logic in AvaloniaPropertyAccessorPlugin. --- .../Plugins/AvaloniaPropertyAccessorPlugin.cs | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 8cbcaa82335..7c12a0d5aea 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -60,35 +60,7 @@ public IPropertyAccessor Start(WeakReference reference, 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) From 2be443a5c4c68316c0ef7fe13ea33754084ed357 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 9 Jun 2018 22:42:20 -0500 Subject: [PATCH 07/12] Fix missing type resolver in TemplateBindingExtension. --- .../MarkupExtensions/TemplateBindingExtension.cs | 4 ++++ .../Avalonia.Markup/Markup/Parsers/ExpressionParser.cs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs index c8fc8a41d1f..e033762d0d7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs @@ -6,8 +6,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { using System; + using System.ComponentModel; using Avalonia.Data.Converters; using Avalonia.Markup.Data; + using Portable.Xaml.ComponentModel; using Portable.Xaml.Markup; [MarkupExtensionReturnType(typeof(IBinding))] @@ -24,6 +26,7 @@ public TemplateBindingExtension(string path) public override object ProvideValue(IServiceProvider serviceProvider) { + var context = (ITypeDescriptorContext)serviceProvider; return new Binding { Converter = Converter, @@ -32,6 +35,7 @@ public override object ProvideValue(IServiceProvider serviceProvider) RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), Path = Path ?? string.Empty, Priority = Priority, + TypeResolver = context.ResolveType }; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 6919eeeb0dd..95bb4217772 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -160,6 +160,11 @@ private State ParseAttachedProperty(Reader r, List nodes) throw new ExpressionParseException(r.Position, "Expected ')'."); } + 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)); From 1b2d644e48a1d1fc4ec87fe7827f619cf0eb32fa Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 18 Jun 2018 17:40:05 -0500 Subject: [PATCH 08/12] Make tests in Avalonia.Base.UnitTests use ExpressionObserver.Create. For tests that require using invalid members or are more tedious to test with expression trees, test them in Avalonia.Markup.UnitTests with ExpressionObserverBuilder. --- .../Data/Core/IndexerExpressionNode.cs | 14 +- .../Parsers/ExpressionVisitorNodeBuilder.cs | 27 +- .../Data/Core/StreamBindingExtensions.cs | 5 + .../Parsers/ExpressionObserverBuilder.cs | 2 +- .../Data/Core/BindingExpressionTests.cs | 40 +- ...xpressionObserverTests_AttachedProperty.cs | 47 +-- ...xpressionObserverTests_AvaloniaProperty.cs | 10 +- .../ExpressionObserverTests_DataValidation.cs | 31 +- .../ExpressionObserverTests_ExpressionTree.cs | 16 + .../Core/ExpressionObserverTests_Indexer.cs | 88 +---- .../Core/ExpressionObserverTests_Lifetime.cs | 16 +- .../Core/ExpressionObserverTests_Negation.cs | 96 +---- .../ExpressionObserverTests_Observable.cs | 23 +- .../Core/ExpressionObserverTests_Property.cs | 119 ++---- .../Core/ExpressionObserverTests_SetValue.cs | 28 +- .../Data/Core/ExpressionObserverTests_Task.cs | 14 +- .../Parsers/ExpressionNodeBuilderTests.cs | 9 + ...onObserverBuilderTests_AttachedProperty.cs | 165 ++++++++ ...onObserverBuilderTests_AvaloniaProperty.cs | 59 +++ .../ExpressionObserverBuilderTests_Indexer.cs | 371 ++++++++++++++++++ .../ExpressionObserverBuilderTests_Method.cs} | 4 +- ...ExpressionObserverBuilderTests_Negation.cs | 112 ++++++ ...ExpressionObserverBuilderTests_Property.cs | 42 ++ 23 files changed, 959 insertions(+), 379 deletions(-) create mode 100644 tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs create mode 100644 tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs create mode 100644 tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs rename tests/{Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs => Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs} (96%) create mode 100644 tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs create mode 100644 tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs diff --git a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs index 273cbd82410..a14802a4339 100644 --- a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq.Expressions; +using System.Reflection; using System.Text; using Avalonia.Data; @@ -48,12 +49,21 @@ public override bool SetTargetValue(object value, BindingPriority priority) protected override object GetValue(object target) { - return getDelegate.DynamicInvoke(target); + try + { + return getDelegate.DynamicInvoke(target); + } + catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException + || e.InnerException is IndexOutOfRangeException + || e.InnerException is KeyNotFoundException) + { + return AvaloniaProperty.UnsetValue; + } } protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) { - return expression.Indexer.Name == e.PropertyName; + return expression.Indexer == null || expression.Indexer.Name == e.PropertyName; } protected override int? TryGetFirstArgumentAsInt() => firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index f1f082174a1..dba9078423f 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -10,6 +10,7 @@ namespace Avalonia.Data.Core.Parsers { class ExpressionVisitorNodeBuilder : ExpressionVisitor { + private const string MultiDimensionalArrayGetterMethodName = "Get"; private static PropertyInfo AvaloniaObjectIndexer; private static MethodInfo CreateDelegateMethod; @@ -98,7 +99,6 @@ protected override Expression VisitBinary(BinaryExpression node) { if (node.NodeType == ExpressionType.ArrayIndex) { - base.VisitBinary(node); return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right })); } throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); @@ -161,21 +161,13 @@ protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) protected override Expression VisitMethodCall(MethodCallExpression node) { - var property = TryGetPropertyFromMethod(node.Method); - - if (property != null) - { - return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); - } - if (node.Method == CreateDelegateMethod) { var visited = Visit(node.Arguments[1]); Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue(node.Object).Name, enableDataValidation)); - return visited; + return node; } - - if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`')) + else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`')) { if (node.Method.IsStatic) { @@ -189,7 +181,18 @@ protected override Expression VisitMethodCall(MethodCallExpression node) return node; } - throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + var property = TryGetPropertyFromMethod(node.Method); + + if (property != null) + { + return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); + } + else if (node.Object.Type.IsArray && node.Method.Name == MultiDimensionalArrayGetterMethodName) + { + return Visit(Expression.MakeIndex(node.Object, null, node.Arguments)); + } + + throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType.AssemblyQualifiedName}.{node.Method.Name}'."); } private PropertyInfo TryGetPropertyFromMethod(MethodInfo method) diff --git a/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs index 907a4ac737a..fa8b56765c3 100644 --- a/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs +++ b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs @@ -14,6 +14,11 @@ public static T StreamBinding(this Task @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(this IObservable @this) { throw new InvalidOperationException("This should be used only in a binding expression"); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 7141a62cd91..ddbe252fc08 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -6,7 +6,7 @@ namespace Avalonia.Markup.Parsers { - public class ExpressionObserverBuilder + public static class ExpressionObserverBuilder { internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func typeResolver = null) { diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index 4e595684df0..f8e050df854 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -23,7 +23,7 @@ public class BindingExpressionTests : IClassFixture public async Task Should_Get_Simple_Property_Value() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string)); var result = await target.Take(1); Assert.Equal("foo", result); @@ -35,7 +35,7 @@ public async Task Should_Get_Simple_Property_Value() public void Should_Set_Simple_Property_Value() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string)); target.OnNext("bar"); @@ -48,7 +48,7 @@ public void Should_Set_Simple_Property_Value() public void Should_Set_Indexed_Value() { var data = new { Foo = new[] { "foo" } }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "Foo[0]"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.Foo[0]), typeof(string)); target.OnNext("bar"); @@ -61,7 +61,7 @@ public void Should_Set_Indexed_Value() public async Task Should_Convert_Get_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); var result = await target.Take(1); Assert.Equal(5.6, result); @@ -73,7 +73,7 @@ public async Task Should_Convert_Get_String_To_Double() public async Task Getting_Invalid_Double_String_Should_Return_BindingError() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); var result = await target.Take(1); Assert.IsType(result); @@ -85,7 +85,7 @@ public async Task Getting_Invalid_Double_String_Should_Return_BindingError() public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() { var data = new Class1 { StringValue = null }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -97,7 +97,7 @@ public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() public void Should_Convert_Set_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); target.OnNext(6.7); @@ -110,7 +110,7 @@ public void Should_Convert_Set_String_To_Double() public async Task Should_Convert_Get_Double_To_String() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); var result = await target.Take(1); Assert.Equal($"{5.6}", result); @@ -122,7 +122,7 @@ public async Task Should_Convert_Get_Double_To_String() public void Should_Convert_Set_Double_To_String() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext($"{6.7}"); @@ -136,7 +136,7 @@ public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonCo { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "StringValue"), + ExpressionObserver.Create(data, o => o.StringValue), typeof(int), 42, DefaultValueConverter.Instance); @@ -157,7 +157,7 @@ public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonCo { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "StringValue", true), + ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), 42, DefaultValueConverter.Instance); @@ -178,7 +178,7 @@ public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue() { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "StringValue"), + ExpressionObserver.Create(data, o => o.StringValue), typeof(int), "bar", DefaultValueConverter.Instance); @@ -200,7 +200,7 @@ public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue_Wi { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "StringValue", true), + ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), "bar", DefaultValueConverter.Instance); @@ -221,7 +221,7 @@ public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue_Wi public void Setting_Invalid_Double_String_Should_Not_Change_Target() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext("foo"); @@ -235,7 +235,7 @@ public void Setting_Invalid_Double_String_Should_Use_FallbackValue() { var data = new Class1 { DoubleValue = 5.6 }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "DoubleValue"), + ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), "9.8", DefaultValueConverter.Instance); @@ -251,7 +251,7 @@ public void Setting_Invalid_Double_String_Should_Use_FallbackValue() public void Should_Coerce_Setting_Null_Double_To_Default_Value() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext(null); @@ -264,7 +264,7 @@ public void Should_Coerce_Setting_Null_Double_To_Default_Value() public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext(AvaloniaProperty.UnsetValue); @@ -280,7 +280,7 @@ public void Should_Pass_ConverterParameter_To_Convert() var converter = new Mock(); var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "DoubleValue"), + ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), converter.Object, converterParameter: "foo"); @@ -298,7 +298,7 @@ public void Should_Pass_ConverterParameter_To_ConvertBack() var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "DoubleValue"), + ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), converter.Object, converterParameter: "foo"); @@ -315,7 +315,7 @@ public void Should_Handle_DataValidation() { var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue", true), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue, true), typeof(string)); var result = new List(); target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs index 112a7fc4d0a..7e47e9b1eb6 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs @@ -8,25 +8,17 @@ using Avalonia.Diagnostics; using Avalonia.Data.Core; using Xunit; -using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { public class ExpressionObserverTests_AttachedProperty { - private readonly Func _typeResolver; - - public ExpressionObserverTests_AttachedProperty() - { - var foo = Owner.FooProperty; - _typeResolver = (_, name) => name == "Owner" ? typeof(Owner) : null; - } [Fact] public async Task Should_Get_Attached_Property_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]); var result = await target.Take(1); Assert.Equal("foo", result); @@ -34,19 +26,6 @@ public async Task Should_Get_Attached_Property_Value() Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); } - [Fact] - public async Task Should_Get_Attached_Property_Value_With_Namespace() - { - var data = new Class1(); - var target = ExpressionObserverBuilder.Build( - data, - "(NS:Owner.Foo)", - typeResolver: (ns, name) => ns == "NS" && name == "Owner" ? typeof(Owner) : null); - var result = await target.Take(1); - Assert.Equal("foo", result); - Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); - } - [Fact] public async Task Should_Get_Chained_Attached_Property_Value() { @@ -58,7 +37,7 @@ public async Task Should_Get_Chained_Attached_Property_Value() } }; - var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -70,7 +49,7 @@ public async Task Should_Get_Chained_Attached_Property_Value() public void Should_Track_Simple_Attached_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -94,7 +73,7 @@ public void Should_Track_Chained_Attached_Value() } }; - var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -113,7 +92,7 @@ public void Should_Not_Keep_Source_Alive() Func> run = () => { var source = new Class1(); - var target = ExpressionObserverBuilder.Build(source, "(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(source, o => o.Next[Owner.FooProperty]); return Tuple.Create(target, new WeakReference(source)); }; @@ -125,22 +104,6 @@ public void Should_Not_Keep_Source_Alive() Assert.Null(result.Item2.Target); } - [Fact] - public void Should_Fail_With_Attached_Property_With_Only_1_Part() - { - var data = new Class1(); - - Assert.Throws(() => 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(() => ExpressionObserverBuilder.Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver)); - } - private static class Owner { public static readonly AttachedProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs index b34c1ff8be7..8d5510dd20e 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs @@ -23,7 +23,7 @@ public ExpressionObserverTests_AvaloniaProperty() public async Task Should_Get_Simple_Property_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Equal("foo", result); @@ -35,7 +35,7 @@ public async Task Should_Get_Simple_Property_Value() public async Task Should_Get_Simple_ClrProperty_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "ClrProperty"); + var target = ExpressionObserver.Create(data, o => o.ClrProperty); var result = await target.Take(1); Assert.Equal("clr-property", result); @@ -45,7 +45,7 @@ public async Task Should_Get_Simple_ClrProperty_Value() public void Should_Track_Simple_Property_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -64,7 +64,7 @@ public void Should_Not_Keep_Source_Alive() Func> run = () => { var source = new Class1(); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); return Tuple.Create(target, new WeakReference(source)); }; @@ -81,6 +81,8 @@ private class Class1 : AvaloniaObject public static readonly StyledProperty FooProperty = AvaloniaProperty.Register("Foo", defaultValue: "foo"); + public string Foo { get => GetValue(FooProperty); set => SetValue(FooProperty, value); } + public string ClrProperty { get; } = "clr-property"; } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index c5f7d1d7bf8..b66dd610dd0 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -20,7 +20,7 @@ public class ExpressionObserverTests_DataValidation : IClassFixture o.MustBePositive, false); var validationMessageFound = false; observer.OfType() @@ -37,7 +37,7 @@ public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() public void Exception_Validation_Sends_DataValidationError() { var data = new ExceptionTest { MustBePositive = 5 }; - var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var validationMessageFound = false; observer.OfType() @@ -54,7 +54,7 @@ public void Exception_Validation_Sends_DataValidationError() public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), false); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); observer.Subscribe(_ => { }); @@ -65,7 +65,7 @@ public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enable public void Enabled_Indei_Validation_Subscribes() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var sub = observer.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); @@ -77,7 +77,7 @@ public void Enabled_Indei_Validation_Subscribes() public void Validation_Plugins_Send_Correct_Notifications() { var data = new IndeiTest(); - var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var result = new List(); var errmsg = string.Empty; @@ -123,10 +123,7 @@ public void Doesnt_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain() Inner = new IndeiTest() }; - var observer = ExpressionObserverBuilder.Build( - data, - $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", - true); + var observer = ExpressionObserver.Create(data, o => o.Inner.MustBePositive, true); observer.Subscribe(_ => { }); @@ -134,19 +131,16 @@ public void Doesnt_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain() // intermediate object in a chain so for the moment I'm not sure what the result of // validating such a thing should look like. Assert.Equal(0, data.ErrorsChangedSubscriptionCount); - Assert.Equal(1, ((IndeiTest)data.Inner).ErrorsChangedSubscriptionCount); + Assert.Equal(1, data.Inner.ErrorsChangedSubscriptionCount); } [Fact] public void Sends_Correct_Notifications_With_Property_Chain() { var container = new Container(); - var inner = new IndeiTest(); - var observer = ExpressionObserverBuilder.Build( - container, - $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", - true); + var observer = ExpressionObserver.Create(container, o => o.Inner.MustBePositive, true); + var result = new List(); observer.Subscribe(x => result.Add(x)); @@ -154,13 +148,12 @@ public void Sends_Correct_Notifications_With_Property_Chain() Assert.Equal(new[] { new BindingNotification( - new MarkupBindingChainException("Null value", "Inner.MustBePositive", "Inner"), + new MarkupBindingChainException("Null value", "o => o.Inner.MustBePositive", "Inner"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, result); GC.KeepAlive(container); - GC.KeepAlive(inner); } public class ExceptionTest : NotifyingBase @@ -221,9 +214,9 @@ public override IEnumerable GetErrors(string propertyName) private class Container : IndeiBase { - private object _inner; + private IndeiTest _inner; - public object Inner + public IndeiTest Inner { get { return _inner; } set { _inner = value; RaisePropertyChanged(); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs index 58f3b22abd5..9b587d76793 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs @@ -181,6 +181,17 @@ public void Should_Get_Completed_Task_Value() } } + [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(value); + GC.KeepAlive(data); + } + private class Class1 : NotifyingBase { private string _foo; @@ -204,5 +215,10 @@ private class Class2 : AvaloniaObject public string ClrProperty { get; } = "clr-property"; } + + private class Class3 + { + public void Method() { } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs index da167e50088..cbbb5f4715c 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs @@ -21,7 +21,7 @@ public class ExpressionObserverTests_Indexer public async Task Should_Get_Array_Value() { var data = new { Foo = new [] { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, x => x.Foo[1]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -29,47 +29,11 @@ public async Task Should_Get_Array_Value() 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 { { 1, "foo" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Get_UnsetValue_For_Object_Without_Indexer() - { - var data = new { Foo = 5 }; - var target = ExpressionObserverBuilder.Build(data, "Foo[noindexer]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - [Fact] public async Task Should_Get_MultiDimensional_Array_Value() { var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1, 1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1, 1]); var result = await target.Take(1); Assert.Equal("qux", result); @@ -81,7 +45,7 @@ public async Task Should_Get_MultiDimensional_Array_Value() public async Task Should_Get_Value_For_String_Indexer() { var data = new { Foo = new Dictionary { { "foo", "bar" }, { "baz", "qux" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -93,7 +57,7 @@ public async Task Should_Get_Value_For_String_Indexer() public async Task Should_Get_Value_For_Non_String_Indexer() { var data = new { Foo = new Dictionary { { 1.0, "bar" }, { 2.0, "qux" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1.0]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1.0]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -105,19 +69,7 @@ public async Task Should_Get_Value_For_Non_String_Indexer() public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue() - { - var data = new { Foo = new[] { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1,2]"); + var target = ExpressionObserver.Create(data, o => o.Foo[2]); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -129,7 +81,7 @@ public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue() public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new List { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); + var target = ExpressionObserver.Create(data, o => o.Foo[2]); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -141,7 +93,7 @@ public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() public async Task Should_Get_List_Value() { var data = new { Foo = new List { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -153,7 +105,7 @@ public async Task Should_Get_List_Value() public void Should_Track_INCC_Add() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); + var target = ExpressionObserver.Create(data, o => o.Foo[2]); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -171,7 +123,7 @@ public void Should_Track_INCC_Add() public void Should_Track_INCC_Remove() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[0]"); + var target = ExpressionObserver.Create(data, o => o.Foo[0]); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -189,7 +141,7 @@ public void Should_Track_INCC_Remove() public void Should_Track_INCC_Replace() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -210,7 +162,7 @@ public void Should_Track_INCC_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 { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -226,7 +178,7 @@ public void Should_Track_INCC_Move() public void Should_Track_INCC_Reset() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -245,7 +197,7 @@ public void Should_Track_NonIntegerIndexer() data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -264,7 +216,7 @@ public void Should_Track_NonIntegerIndexer() public void Should_SetArrayIndex() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); using (target.Subscribe(_ => { })) { @@ -286,8 +238,8 @@ public void Should_Set_ExistingDictionaryEntry() {"foo", 1 } } }; - - var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); using (target.Subscribe(_ => { })) { Assert.True(target.SetValue(4)); @@ -308,8 +260,8 @@ public void Should_Add_NewDictionaryEntry() {"foo", 1 } } }; - - var target = ExpressionObserverBuilder.Build(data, "Foo[bar]"); + + var target = ExpressionObserver.Create(data, o => o.Foo["bar"]); using (target.Subscribe(_ => { })) { Assert.True(target.SetValue(4)); @@ -327,7 +279,7 @@ public void Should_Set_NonIntegerIndexer() data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); using (target.Subscribe(_ => { })) { @@ -344,7 +296,7 @@ public async Task Indexer_Only_Binding_Works() { var data = new[] { 1, 2, 3 }; - var target = ExpressionObserverBuilder.Build(data, "[1]"); + var target = ExpressionObserver.Create(data, o => o[1]); var value = await target.Take(1); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs index 4f3980e59a8..cf151f62445 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs @@ -19,7 +19,7 @@ public class ExpressionObserverTests_Lifetime public void Should_Complete_When_Source_Observable_Completes() { var source = new BehaviorSubject(1); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -32,7 +32,7 @@ public void Should_Complete_When_Source_Observable_Completes() public void Should_Complete_When_Source_Observable_Errors() { var source = new BehaviorSubject(1); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -45,7 +45,7 @@ public void Should_Complete_When_Source_Observable_Errors() public void Should_Complete_When_Update_Observable_Completes() { var update = new Subject(); - var target = ExpressionObserverBuilder.Build(() => 1, "Foo", update); + var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -58,7 +58,7 @@ public void Should_Complete_When_Update_Observable_Completes() public void Should_Complete_When_Update_Observable_Errors() { var update = new Subject(); - var target = ExpressionObserverBuilder.Build(() => 1, "Foo", update); + var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -73,7 +73,7 @@ public void Should_Unsubscribe_From_Source_Observable() var scheduler = new TestScheduler(); var source = scheduler.CreateColdObservable( OnNext(1, new { Foo = "foo" })); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -92,7 +92,7 @@ public void Should_Unsubscribe_From_Update_Observable() var scheduler = new TestScheduler(); var update = scheduler.CreateColdObservable(); var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(() => data, "Foo", update); + var target = ExpressionObserver.Create(() => data, o => o.Foo, update); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -107,9 +107,9 @@ public void Should_Unsubscribe_From_Update_Observable() GC.KeepAlive(data); } - private Recorded> OnNext(long time, object value) + private Recorded> OnNext(long time, T value) { - return new Recorded>(time, Notification.CreateOnNext(value)); + return new Recorded>(time, Notification.CreateOnNext(value)); } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs index 9ca1d07ee0e..54d7e98903f 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs @@ -17,7 +17,7 @@ public class ExpressionObserverTests_Negation public async Task Should_Negate_Boolean_Value() { var data = new { Foo = true }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var target = ExpressionObserver.Create(data, o => !o.Foo); var result = await target.Take(1); Assert.False((bool)result); @@ -25,103 +25,11 @@ public async Task Should_Negate_Boolean_Value() GC.KeepAlive(data); } - [Fact] - public async Task Should_Negate_0() - { - var data = new { Foo = 0 }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.True((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Negate_1() - { - var data = new { Foo = 1 }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.False((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Negate_False_String() - { - var data = new { Foo = "false" }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.True((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Negate_True_String() - { - var data = new { Foo = "True" }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.False((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() - { - var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.Equal( - new BindingNotification( - new InvalidCastException($"Unable to convert 'foo' to bool."), - BindingErrorType.Error), - result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() - { - var data = new { Foo = new object() }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.Equal( - new BindingNotification( - new InvalidCastException($"Unable to convert 'System.Object' to bool."), - BindingErrorType.Error), - result); - - GC.KeepAlive(data); - } - - [Fact] - public void SetValue_Should_Return_False_For_Invalid_Value() - { - var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - target.Subscribe(_ => { }); - - Assert.False(target.SetValue("bar")); - - GC.KeepAlive(data); - } - [Fact] public void Can_SetValue_For_Valid_Value() { var data = new Test { Foo = true }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var target = ExpressionObserver.Create(data, o => !o.Foo); target.Subscribe(_ => { }); Assert.True(target.SetValue(true)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs index 2a8ead4e485..701fdbce9c9 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs @@ -16,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core public class ExpressionObserverTests_Observable { [Fact] - public void Should_Not_Get_Observable_Value_Without_Modifier_Char() + public void Should_Not_Get_Observable_Value_Without_Streaming() { using (var sync = UnitTestSynchronizationContext.Begin()) { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -42,7 +42,7 @@ public void Should_Get_Simple_Observable_Value() { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = ExpressionObserverBuilder.Build(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -61,7 +61,7 @@ public void Should_Get_Property_Value_From_Observable() using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Next^.Foo"); + var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -84,7 +84,7 @@ public void Should_Get_Simple_Observable_Value_With_DataValidation_Enabled() { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = ExpressionObserverBuilder.Build(data, "Foo^", true); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -106,7 +106,7 @@ public void Should_Get_Property_Value_From_Observable_With_DataValidation_Enable { var data1 = new Class1(); var data2 = new Class2("foo"); - var target = ExpressionObserverBuilder.Build(data1, "Next^.Foo", true); + var target = ExpressionObserver.Create(data1, o => o.Next.StreamBinding().Foo, true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -128,8 +128,8 @@ public void Should_Return_BindingNotification_If_Stream_Operator_Applied_To_Not_ { using (var sync = UnitTestSynchronizationContext.Begin()) { - var data = new Class2("foo"); - var target = ExpressionObserverBuilder.Build(data, "Foo^", true); + var data = new NotStreamable(); + var target = ExpressionObserver.Create(data, o => o.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -139,7 +139,7 @@ public void Should_Return_BindingNotification_If_Stream_Operator_Applied_To_Not_ new[] { new BindingNotification( - new MarkupBindingChainException("Stream operator applied to unsupported type", "Foo^", "Foo^"), + new MarkupBindingChainException("Stream operator applied to unsupported type", "o => o.StreamBinding()", "^"), BindingErrorType.Error) }, result); @@ -164,5 +164,10 @@ public Class2(string foo) public string Foo { get; } } + + private class NotStreamable + { + public object StreamBinding() { throw new InvalidOperationException(); } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 9381d073222..c90683959ee 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -22,7 +22,7 @@ public class ExpressionObserverTests_Property public async Task Should_Get_Simple_Property_Value() { var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Equal("foo", result); @@ -34,7 +34,7 @@ public async Task Should_Get_Simple_Property_Value() public void Should_Get_Simple_Property_Value_Type() { var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); target.Subscribe(_ => { }); @@ -47,7 +47,7 @@ public void Should_Get_Simple_Property_Value_Type() public async Task Should_Get_Simple_Property_Value_Null() { var data = new { Foo = (string)null }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Null(result); @@ -59,7 +59,7 @@ public async Task Should_Get_Simple_Property_Value_Null() public async Task Should_Get_Simple_Property_From_Base_Class() { var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Equal("foo", result); @@ -70,76 +70,65 @@ public async Task Should_Get_Simple_Property_From_Base_Class() [Fact] public async Task Should_Return_BindingNotification_Error_For_Root_Null() { - var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(default(object), "Foo"); + var target = ExpressionObserver.Create(default(Class3), o => o.Foo); var result = await target.Take(1); Assert.Equal( new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); - - GC.KeepAlive(data); } [Fact] public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue() { - var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(AvaloniaProperty.UnsetValue, "Foo"); + var target = ExpressionObserver.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo); var result = await target.Take(1); Assert.Equal( new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); - - GC.KeepAlive(data); } [Fact] public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null() { - var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(Observable.Return(default(object)), "Foo"); + var target = ExpressionObserver.Create(Observable.Return(default(Class3)), o => o.Foo); var result = await target.Take(1); Assert.Equal( new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); - - GC.KeepAlive(data); } [Fact] public async void Should_Return_BindingNotification_Error_For_Observable_Root_UnsetValue() { - var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(Observable.Return(AvaloniaProperty.UnsetValue), "Foo"); + var target = ExpressionObserver.Create(Observable.Return(AvaloniaProperty.UnsetValue), o => (o as Class3).Foo); var result = await target.Take(1); Assert.Equal( new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); - - GC.KeepAlive(data); + } [Fact] public async Task Should_Get_Simple_Property_Chain() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz); var result = await target.Take(1); Assert.Equal("baz", result); @@ -151,7 +140,7 @@ public async Task Should_Get_Simple_Property_Chain() public void Should_Get_Simple_Property_Chain_Type() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz); target.Subscribe(_ => { }); @@ -160,28 +149,11 @@ public void Should_Get_Simple_Property_Chain_Type() GC.KeepAlive(data); } - [Fact] - public async Task Should_Return_BindingNotification_Error_For_Broken_Chain() - { - var data = new { Foo = new { Bar = 1 } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); - var result = await target.Take(1); - - Assert.IsType(result); - - Assert.Equal( - new BindingNotification( - new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error), - result); - - GC.KeepAlive(data); - } - [Fact] public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value() { - var data = new { Foo = default(object) }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); + var data = new { Foo = default(Class1) }; + var target = ExpressionObserver.Create(data, o => o.Foo.Foo.Length); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -190,7 +162,7 @@ public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value() new[] { new BindingNotification( - new MarkupBindingChainException("Null value", "Foo.Bar.Baz", "Foo"), + new MarkupBindingChainException("Null value", "o => o.Foo.Foo.Length", "Foo"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, @@ -199,22 +171,11 @@ public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value() GC.KeepAlive(data); } - [Fact] - public void Should_Have_Null_ResultType_For_Broken_Chain() - { - var data = new { Foo = new { Bar = 1 } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); - - Assert.Null(target.ResultType); - - GC.KeepAlive(data); - } - [Fact] public void Should_Track_Simple_Property_Value() { var data = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -233,7 +194,7 @@ public void Should_Track_Simple_Property_Value() public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String() { var data = new Class1 { Bar = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Bar"); + var target = ExpressionObserver.Create(data, o => o.Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -263,7 +224,7 @@ public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String() public void Should_Track_End_Of_Property_Chain_Changing() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -284,7 +245,7 @@ public void Should_Track_End_Of_Property_Chain_Changing() public void Should_Track_Property_Chain_Changing() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -317,7 +278,7 @@ public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Next.Bar"); + var target = ExpressionObserver.Create(data, o => ((o.Next as Class2).Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -330,7 +291,7 @@ public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() { "bar", new BindingNotification( - new MarkupBindingChainException("Null value", "Next.Next.Bar", "Next.Next"), + new MarkupBindingChainException("Null value", "o => ((o.Next As Class2).Next As Class2).Bar", "Next.Next"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), "bar" @@ -350,7 +311,7 @@ public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -385,7 +346,7 @@ public void Empty_Expression_Should_Track_Root() { var data = new Class1 { Foo = "foo" }; var update = new Subject(); - var target = ExpressionObserverBuilder.Build(() => data.Foo, "", update); + var target = ExpressionObserver.Create(() => data.Foo, o => o, update); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -405,7 +366,7 @@ public void Should_Track_Property_Value_From_Observable_Root() var source = scheduler.CreateColdObservable( OnNext(1, new Class1 { Foo = "foo" }), OnNext(2, new Class1 { Foo = "bar" })); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -421,7 +382,7 @@ public void Should_Track_Property_Value_From_Observable_Root() public void Subscribing_Multiple_Times_Should_Return_Values_To_All() { var data = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result1 = new List(); var result2 = new List(); var result3 = new List(); @@ -444,7 +405,7 @@ public void Subscribing_Multiple_Times_Should_Return_Values_To_All() public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once() { var data = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var sub1 = target.Subscribe(x => { }); var sub2 = target.Subscribe(x => { }); @@ -463,7 +424,7 @@ public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_ public void SetValue_Should_Set_Simple_Property_Value() { var data = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); using (target.Subscribe(_ => { })) { @@ -479,7 +440,7 @@ public void SetValue_Should_Set_Simple_Property_Value() public void SetValue_Should_Set_Property_At_The_End_Of_Chain() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); using (target.Subscribe(_ => { })) { @@ -495,7 +456,7 @@ public void SetValue_Should_Set_Property_At_The_End_Of_Chain() public void SetValue_Should_Return_False_For_Missing_Property() { var data = new Class1 { Next = new WithoutBar() }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); using (target.Subscribe(_ => { })) { @@ -509,7 +470,7 @@ public void SetValue_Should_Return_False_For_Missing_Property() public void SetValue_Should_Notify_New_Value_With_Inpc() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -524,7 +485,7 @@ public void SetValue_Should_Notify_New_Value_With_Inpc() public void SetValue_Should_Notify_New_Value_Without_Inpc() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Bar"); + var target = ExpressionObserver.Create(data, o => o.Bar); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -539,7 +500,7 @@ public void SetValue_Should_Notify_New_Value_Without_Inpc() public void SetValue_Should_Return_False_For_Missing_Object() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); using (target.Subscribe(_ => { })) { @@ -556,7 +517,7 @@ public void Can_Replace_Root() var second = new Class1 { Foo = "bar" }; var root = first; var update = new Subject(); - var target = ExpressionObserverBuilder.Build(() => root, "Foo", update); + var target = ExpressionObserver.Create(() => root, o => o.Foo, update); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -571,7 +532,7 @@ public void Can_Replace_Root() "foo", "bar", new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue) }, @@ -590,7 +551,7 @@ public void Should_Not_Keep_Source_Alive() Func> run = () => { var source = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); return Tuple.Create(target, new WeakReference(source)); }; @@ -674,9 +635,9 @@ private class WithoutBar : NotifyingBase, INext { } - private Recorded> OnNext(long time, object value) + private Recorded> OnNext(long time, T value) { - return new Recorded>(time, Notification.CreateOnNext(value)); + return new Recorded>(time, Notification.CreateOnNext(value)); } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs index 974ac771555..99507a2c072 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs @@ -17,7 +17,7 @@ public class ExpressionObserverTests_SetValue public void Should_Set_Simple_Property_Value() { var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); using (target.Subscribe(_ => { })) { @@ -31,7 +31,8 @@ public void Should_Set_Simple_Property_Value() public void Should_Set_Value_On_Simple_Property_Chain() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar); + using (target.Subscribe(_ => { })) { @@ -45,14 +46,15 @@ public void Should_Set_Value_On_Simple_Property_Chain() public void Should_Not_Try_To_Set_Value_On_Broken_Chain() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar); // Ensure the ExpressionObserver's subscriptions are kept active. - target.OfType().Subscribe(x => { }); - - data.Foo = null; + using (target.OfType().Subscribe(x => { })) + { + data.Foo = null; + Assert.False(target.SetValue("foo")); + } - Assert.False(target.SetValue("foo")); } /// @@ -68,13 +70,15 @@ public void Pushing_Null_To_RootObservable_Updates_Leaf_Node() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; var rootObservable = new BehaviorSubject(data); - var target = ExpressionObserverBuilder.Build(rootObservable, "Foo.Bar"); + var target = ExpressionObserver.Create(rootObservable, o => o.Foo.Bar); - target.Subscribe(_ => { }); - rootObservable.OnNext(null); - target.SetValue("baz"); + using (target.Subscribe(_ => { })) + { + rootObservable.OnNext(null); + target.SetValue("baz"); + Assert.Equal("bar", data.Foo.Bar); + } - Assert.Equal("bar", data.Foo.Bar); } private class Class1 : NotifyingBase diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs index 69dec53d0e6..9ea0a5e3e18 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs @@ -16,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core public class ExpressionObserverTests_Task { [Fact] - public void Should_Not_Get_Task_Result_Without_Modifier_Char() + public void Should_Not_Get_Task_Result_Without_StreamBinding() { using (var sync = UnitTestSynchronizationContext.Begin()) { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -42,7 +42,7 @@ public void Should_Get_Completed_Task_Value() using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = Task.FromResult("foo") }; - var target = ExpressionObserverBuilder.Build(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -60,7 +60,7 @@ public void Should_Get_Property_Value_From_Task() { var tcs = new TaskCompletionSource(); var data = new Class1(tcs.Task); - var target = ExpressionObserverBuilder.Build(data, "Next^.Foo"); + var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -80,7 +80,7 @@ public void Should_Return_BindingNotification_Error_On_Task_Exception() { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = ExpressionObserverBuilder.Build(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -106,7 +106,7 @@ public void Should_Return_BindingNotification_Error_For_Faulted_Task() using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = TaskFromException(new NotSupportedException()) }; - var target = ExpressionObserverBuilder.Build(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -131,7 +131,7 @@ public void Should_Get_Simple_Task_Value_With_Data_DataValidation_Enabled() { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = ExpressionObserverBuilder.Build(data, "Foo^", true); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs index 8bb23815528..212b16965c2 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs @@ -137,6 +137,15 @@ public void Should_Build_Indexed_Property_In_Chain() AssertIsProperty(result[3], "Baz"); } + [Fact] + public void Should_Build_Stream_Node() + { + var result = ToList(ExpressionObserverBuilder.Parse("Foo^")); + + Assert.Equal(2, result.Count); + Assert.IsType(result[1]); + } + private void AssertIsProperty(ExpressionNode node, string name) { Assert.IsType(node); diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs new file mode 100644 index 00000000000..5b97ab7ae6d --- /dev/null +++ b/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 _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(); + + 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(); + + 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> 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(() => 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(() => ExpressionObserverBuilder.Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver)); + } + + private static class Owner + { + public static readonly AttachedProperty FooProperty = + AvaloniaProperty.RegisterAttached( + "Foo", + typeof(Owner), + defaultValue: "foo"); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty NextProperty = + AvaloniaProperty.Register(nameof(Next)); + + public Class1 Next + { + get { return GetValue(NextProperty); } + set { SetValue(NextProperty, value); } + } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs new file mode 100644 index 00000000000..816185cb642 --- /dev/null +++ b/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(); + + 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 FooProperty = + AvaloniaProperty.Register("Foo", defaultValue: "foo"); + + public string ClrProperty { get; } = "clr-property"; + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs new file mode 100644 index 00000000000..39d6152b690 --- /dev/null +++ b/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 { { 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 { { "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 { { 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 { "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 { "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 { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); + var result = new List(); + + 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 { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[0]"); + var result = new List(); + + 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 { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var result = new List(); + + 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 { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var result = new List(); + + 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 { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var result = new List(); + + 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(); + + 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 + { + {"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 + { + {"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 _storage = new Dictionary(); + + public string this[string key] + { + get + { + return _storage[key]; + } + set + { + _storage[key] = value; + RaisePropertyChanged(CommonPropertyNames.IndexerName); + } + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs similarity index 96% rename from tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs rename to tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs index 6bb448158e8..b0623aa456d 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs @@ -9,9 +9,9 @@ using System.Threading.Tasks; using Xunit; -namespace Avalonia.Base.UnitTests.Data.Core +namespace Avalonia.Markup.UnitTests.Parsers { - public class ExpressionObserverTests_Method + public class ExpressionObserverBuilderTests_Method { private class TestObject { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs new file mode 100644 index 00000000000..24f6407908c --- /dev/null +++ b/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; } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs new file mode 100644 index 00000000000..a97c9982643 --- /dev/null +++ b/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(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); + } + } +} From f759d28720045b88d1b6d83f59f86f7ea0cc0d43 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 18 Jun 2018 18:00:33 -0500 Subject: [PATCH 09/12] Fix merge --- src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs | 6 +++--- src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs | 2 +- src/Avalonia.Base/Data/Core/IndexerNodeBase.cs | 6 +----- src/Avalonia.Base/Data/Core/SettableNode.cs | 2 +- .../Markup/Parsers/Nodes/StringIndexerNode.cs | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 18e853722dc..3484fb5e23e 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Data.Core { - public class AvaloniaPropertyAccessorNode : ExpressionNode, ISettableNode + public class AvaloniaPropertyAccessorNode : SettableNode { private readonly bool _enableValidation; private readonly AvaloniaProperty _property; @@ -18,9 +18,9 @@ public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValida public override string Description => PropertyName; public string PropertyName { get; } - public Type PropertyType => _property.PropertyType; + public override Type PropertyType => _property.PropertyType; - public bool SetTargetValue(object value, BindingPriority priority) + protected override bool SetTargetValueCore(object value, BindingPriority priority) { try { diff --git a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs index a14802a4339..0b0d43a97d4 100644 --- a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -34,7 +34,7 @@ public IndexerExpressionNode(IndexExpression expression) public override string Description => expression.ToString(); - public override bool SetTargetValue(object value, BindingPriority priority) + protected override bool SetTargetValueCore(object value, BindingPriority priority) { try { diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 6dc7d61168c..a43d119ebb0 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -13,7 +13,7 @@ namespace Avalonia.Data.Core { - public abstract class IndexerNodeBase : ExpressionNode, ISettableNode + public abstract class IndexerNodeBase : SettableNode { protected override IObservable StartListeningCore(WeakReference reference) { @@ -41,10 +41,6 @@ protected override IObservable StartListeningCore(WeakReference referenc return inputs.Merge().StartWith(GetValue(target)); } - public abstract bool SetTargetValue(object value, BindingPriority priority); - - public abstract Type PropertyType { get; } - protected abstract object GetValue(object target); protected abstract int? TryGetFirstArgumentAsInt(); diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 092cdbe48fa..e7c6ab766fd 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Data.Core { - internal abstract class SettableNode : ExpressionNode + public abstract class SettableNode : ExpressionNode { public bool SetTargetValue(object value, BindingPriority priority) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index 01eb0a9b53b..b3d0555f14f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -25,7 +25,7 @@ public StringIndexerNode(IList arguments) public override string Description => "[" + string.Join(",", Arguments) + "]"; - public override bool SetTargetValue(object value, BindingPriority priority) + protected override bool SetTargetValueCore(object value, BindingPriority priority) { var typeInfo = Target.Target.GetType().GetTypeInfo(); var list = Target.Target as IList; From dbc942b0666141d308942a2e463c25fd1ba4015f Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 2 Jul 2018 21:27:44 -0500 Subject: [PATCH 10/12] Add XML docs. --- .../Data/Core/ExpressionObserver.cs | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 967a49924a6..07e01498e54 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -65,7 +65,7 @@ public class ExpressionObserver : LightweightObservableBase, IDescriptio /// The root object. /// The expression. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. /// public ExpressionObserver( object root, @@ -82,6 +82,15 @@ public ExpressionObserver( _root = new WeakReference(root); } + /// + /// Creates a new instance of the class. + /// + /// The root object. + /// The expression. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// public static ExpressionObserver Create( T root, Expression> expression, @@ -97,7 +106,7 @@ public static ExpressionObserver Create( /// An observable which provides the root object. /// The expression. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. /// public ExpressionObserver( IObservable rootObservable, @@ -110,7 +119,16 @@ public ExpressionObserver( Description = description; _root = rootObservable; } - + + /// + /// Creates a new instance of the class. + /// + /// An observable which provides the root object. + /// The expression. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// public static ExpressionObserver Create( IObservable rootObservable, Expression> expression, @@ -132,7 +150,7 @@ public static ExpressionObserver Create( /// The expression. /// An observable which triggers a re-read of the getter. /// - /// A description of the expression. If null, will be used. + /// A description of the expression. /// public ExpressionObserver( Func rootGetter, @@ -148,6 +166,16 @@ public ExpressionObserver( _root = update.Select(x => rootGetter()); } + /// + /// Creates a new instance of the class. + /// + /// A function which gets the root object. + /// The expression. + /// An observable which triggers a re-read of the getter. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// public static ExpressionObserver Create( Func rootGetter, Expression> expression, From e76eda1abf50c37d056a757ec022979666677d3c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 2 Jul 2018 22:36:47 -0500 Subject: [PATCH 11/12] Add back missing typeResolver arguments removed in the merge. --- src/Markup/Avalonia.Markup/Data/Binding.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 6f3d736be4c..e5f7ea17422 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -199,7 +199,8 @@ private ExpressionObserver CreateDataContextObserver( () => target.GetValue(StyledElement.DataContextProperty), path, new UpdateSignal(target, StyledElement.DataContextProperty), - enableDataValidation); + enableDataValidation, + typeResolver: TypeResolver); return result; } @@ -287,7 +288,8 @@ private ExpressionObserver CreateTemplatedParentObserver( () => target.GetValue(StyledElement.TemplatedParentProperty), path, new UpdateSignal(target, StyledElement.TemplatedParentProperty), - enableDataValidation); + enableDataValidation, + typeResolver: TypeResolver); return result; } From f6fc30665e72c7dcbe7493e037b7e97a822ae2ab Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 8 Jul 2018 18:57:41 -0500 Subject: [PATCH 12/12] Fix nits. --- .../Data/Core/ExpressionObserver.cs | 85 +++++++++---------- .../Data/Core/IndexerExpressionNode.cs | 32 +++---- .../Data/Core/Parsers/ExpressionTreeParser.cs | 11 +-- .../Parsers/ExpressionVisitorNodeBuilder.cs | 10 +-- 4 files changed, 65 insertions(+), 73 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 07e01498e54..773049d3a5b 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -82,24 +82,6 @@ public ExpressionObserver( _root = new WeakReference(root); } - /// - /// Creates a new instance of the class. - /// - /// The root object. - /// The expression. - /// Whether or not to track data validation - /// - /// A description of the expression. If null, 's string representation will be used. - /// - public static ExpressionObserver Create( - T root, - Expression> expression, - bool enableDataValidation = false, - string description = null) - { - return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString()); - } - /// /// Initializes a new instance of the class. /// @@ -119,29 +101,6 @@ public ExpressionObserver( Description = description; _root = rootObservable; } - - /// - /// Creates a new instance of the class. - /// - /// An observable which provides the root object. - /// The expression. - /// Whether or not to track data validation - /// - /// A description of the expression. If null, 's string representation will be used. - /// - public static ExpressionObserver Create( - IObservable rootObservable, - Expression> expression, - bool enableDataValidation = false, - string description = null) - { - Contract.Requires(rootObservable != null); - return new ExpressionObserver( - rootObservable.Select(o => (object)o), - Parse(expression, enableDataValidation), - description ?? expression.ToString()); - - } /// /// Initializes a new instance of the class. @@ -166,6 +125,47 @@ public ExpressionObserver( _root = update.Select(x => rootGetter()); } + + /// + /// Creates a new instance of the class. + /// + /// The root object. + /// The expression. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// + public static ExpressionObserver Create( + T root, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString()); + } + + /// + /// Creates a new instance of the class. + /// + /// An observable which provides the root object. + /// The expression. + /// Whether or not to track data validation + /// + /// A description of the expression. If null, 's string representation will be used. + /// + public static ExpressionObserver Create( + IObservable rootObservable, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootObservable != null); + return new ExpressionObserver( + rootObservable.Select(o => (object)o), + Parse(expression, enableDataValidation), + description ?? expression.ToString()); + } + /// /// Creates a new instance of the class. /// @@ -278,8 +278,7 @@ protected override void Subscribed(IObserver observer, bool first) private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) { - var parser = new ExpressionTreeParser(enableDataValidation); - return parser.Parse(expression); + return ExpressionTreeParser.Parse(expression, enableDataValidation); } private void StartRoot() diff --git a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs index 0b0d43a97d4..04412b61efe 100644 --- a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -10,35 +10,35 @@ 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; + 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); - this.expression = expression.Update(parameter, expression.Arguments); + _parameter = Expression.Parameter(expression.Object.Type); + _expression = expression.Update(_parameter, expression.Arguments); - getDelegate = Expression.Lambda(this.expression, parameter).Compile(); + _getDelegate = Expression.Lambda(_expression, _parameter).Compile(); var valueParameter = Expression.Parameter(expression.Type); - setDelegate = Expression.Lambda(Expression.Assign(this.expression, valueParameter), parameter, valueParameter).Compile(); + _setDelegate = Expression.Lambda(Expression.Assign(_expression, valueParameter), _parameter, valueParameter).Compile(); - firstArgumentDelegate = Expression.Lambda(this.expression.Arguments[0], parameter).Compile(); + _firstArgumentDelegate = Expression.Lambda(_expression.Arguments[0], _parameter).Compile(); } - public override Type PropertyType => expression.Type; + public override Type PropertyType => _expression.Type; - public override string Description => expression.ToString(); + public override string Description => _expression.ToString(); protected override bool SetTargetValueCore(object value, BindingPriority priority) { try { - setDelegate.DynamicInvoke(Target.Target, value); + _setDelegate.DynamicInvoke(Target.Target, value); return true; } catch (Exception) @@ -51,7 +51,7 @@ protected override object GetValue(object target) { try { - return getDelegate.DynamicInvoke(target); + return _getDelegate.DynamicInvoke(target); } catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException || e.InnerException is IndexOutOfRangeException @@ -63,9 +63,9 @@ protected override object GetValue(object target) protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) { - return expression.Indexer == null || expression.Indexer.Name == e.PropertyName; + return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName; } - protected override int? TryGetFirstArgumentAsInt() => firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; + protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; } } diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs index 4d6f56667ae..db5d1176874 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs @@ -6,16 +6,9 @@ namespace Avalonia.Data.Core.Parsers { - class ExpressionTreeParser + static class ExpressionTreeParser { - private readonly bool enableDataValidation; - - public ExpressionTreeParser(bool enableDataValidation) - { - this.enableDataValidation = enableDataValidation; - } - - public ExpressionNode Parse(Expression expr) + public static ExpressionNode Parse(Expression expr, bool enableDataValidation) { var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation); diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index dba9078423f..1b4d1c200da 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -14,7 +14,7 @@ class ExpressionVisitorNodeBuilder : ExpressionVisitor private static PropertyInfo AvaloniaObjectIndexer; private static MethodInfo CreateDelegateMethod; - private readonly bool enableDataValidation; + private readonly bool _enableDataValidation; static ExpressionVisitorNodeBuilder() { @@ -26,7 +26,7 @@ static ExpressionVisitorNodeBuilder() public ExpressionVisitorNodeBuilder(bool enableDataValidation) { - this.enableDataValidation = enableDataValidation; + _enableDataValidation = enableDataValidation; Nodes = new List(); } @@ -62,7 +62,7 @@ protected override Expression VisitUnary(UnaryExpression node) protected override Expression VisitMember(MemberExpression node) { var visited = base.VisitMember(node); - Nodes.Add(new PropertyAccessorNode(node.Member.Name, enableDataValidation)); + Nodes.Add(new PropertyAccessorNode(node.Member.Name, _enableDataValidation)); return visited; } @@ -73,7 +73,7 @@ protected override Expression VisitIndex(IndexExpression node) if (node.Indexer == AvaloniaObjectIndexer) { var property = GetArgumentExpressionValue(node.Arguments[0]); - Nodes.Add(new AvaloniaPropertyAccessorNode(property, enableDataValidation)); + Nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableDataValidation)); } else { @@ -164,7 +164,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) if (node.Method == CreateDelegateMethod) { var visited = Visit(node.Arguments[1]); - Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue(node.Object).Name, enableDataValidation)); + Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue(node.Object).Name, _enableDataValidation)); return node; } else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`'))