diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs new file mode 100644 index 0000000000..f7e5de2fe2 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using Avalonia.Reactive; + +namespace Avalonia.Data.Core +{ + public class AvaloniaPropertyAccessorNode : SettableNode + { + private IDisposable _subscription; + private readonly bool _enableValidation; + private readonly AvaloniaProperty _property; + + public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation) + { + _property = property; + _enableValidation = enableValidation; + } + + public override string Description => PropertyName; + public string PropertyName { get; } + public override Type PropertyType => _property.PropertyType; + + protected override bool SetTargetValueCore(object value, BindingPriority priority) + { + try + { + if (Target.IsAlive && Target.Target is IAvaloniaObject obj) + { + obj.SetValue(_property, value, priority); + return true; + } + return false; + } + catch + { + return false; + } + } + + protected override void StartListeningCore(WeakReference reference) + { + if (reference.Target is IAvaloniaObject obj) + { + _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); + } + else + { + _subscription = null; + } + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } + } +} diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index c4166b44e5..0e2c3c035c 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; 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 600cd68d60..9ee4787e47 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -5,7 +5,7 @@ using System; namespace Avalonia.Data.Core { - internal abstract class ExpressionNode + public abstract class ExpressionNode { private static readonly object CacheInvalid = new object(); protected static readonly WeakReference UnsetReference = diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs b/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs deleted file mode 100644 index 8e9e9fc3c1..0000000000 --- a/src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Data.Core.Parsers; - -namespace Avalonia.Data.Core -{ - internal static class ExpressionNodeBuilder - { - public static ExpressionNode Build(string expression, bool enableValidation = false) - { - if (string.IsNullOrWhiteSpace(expression)) - { - throw new ArgumentException("'expression' may not be empty."); - } - - var reader = new Reader(expression); - var parser = new ExpressionParser(enableValidation); - var node = parser.Parse(reader); - - if (!reader.End) - { - throw new ExpressionParseException(reader.Position, "Expected end of expression."); - } - - return node; - } - } -} diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 3a061206bf..773049d3a5 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reactive; using System.Reactive.Linq; using Avalonia.Data; +using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Plugins; using Avalonia.Reactive; @@ -61,27 +63,22 @@ namespace Avalonia.Data.Core /// 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. /// public ExpressionObserver( object root, - string expression, - bool enableDataValidation = false, + ExpressionNode node, string description = null) { - Contract.Requires(expression != null); - if (root == AvaloniaProperty.UnsetValue) { root = null; } - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + _node = node; + Description = description; _root = new WeakReference(root); } @@ -89,23 +86,19 @@ namespace Avalonia.Data.Core /// 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. /// public ExpressionObserver( IObservable rootObservable, - string expression, - bool enableDataValidation = false, - string description = null) + ExpressionNode node, + string description) { Contract.Requires(rootObservable != null); - Contract.Requires(expression != null); - - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + + _node = node; + Description = description; _root = rootObservable; } @@ -113,30 +106,92 @@ namespace Avalonia.Data.Core /// 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. /// public ExpressionObserver( Func rootGetter, - string expression, + ExpressionNode node, IObservable update, - bool enableDataValidation = false, - string description = null) + string description) { Contract.Requires(rootGetter != null); - Contract.Requires(expression != null); Contract.Requires(update != null); - - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + Description = description; + _node = node; _node.Target = new WeakReference(rootGetter()); _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. + /// + /// 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, + 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. /// @@ -221,16 +276,9 @@ namespace Avalonia.Data.Core } } - private static ExpressionNode Parse(string expression, bool enableDataValidation) + private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) { - if (!string.IsNullOrWhiteSpace(expression)) - { - return ExpressionNodeBuilder.Build(expression, enableDataValidation); - } - else - { - return new EmptyExpressionNode(); - } + return ExpressionTreeParser.Parse(expression, enableDataValidation); } private void StartRoot() diff --git a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs index 3d7bce4080..1845b1b52a 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionParseException.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionParseException.cs @@ -17,8 +17,8 @@ namespace Avalonia.Data.Core /// /// 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/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs new file mode 100644 index 0000000000..04412b61ef --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using Avalonia.Data; + +namespace Avalonia.Data.Core +{ + class IndexerExpressionNode : IndexerNodeBase + { + private readonly ParameterExpression _parameter; + private readonly IndexExpression _expression; + private readonly Delegate _setDelegate; + private readonly Delegate _getDelegate; + private readonly Delegate _firstArgumentDelegate; + + public IndexerExpressionNode(IndexExpression expression) + { + _parameter = Expression.Parameter(expression.Object.Type); + _expression = expression.Update(_parameter, expression.Arguments); + + _getDelegate = Expression.Lambda(_expression, _parameter).Compile(); + + var valueParameter = Expression.Parameter(expression.Type); + + _setDelegate = Expression.Lambda(Expression.Assign(_expression, valueParameter), _parameter, valueParameter).Compile(); + + _firstArgumentDelegate = Expression.Lambda(_expression.Arguments[0], _parameter).Compile(); + } + + public override Type PropertyType => _expression.Type; + + public override string Description => _expression.ToString(); + + protected override bool SetTargetValueCore(object value, BindingPriority priority) + { + try + { + _setDelegate.DynamicInvoke(Target.Target, value); + return true; + } + catch (Exception) + { + return false; + } + } + + protected override object GetValue(object target) + { + try + { + return _getDelegate.DynamicInvoke(target); + } + catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException + || e.InnerException is IndexOutOfRangeException + || e.InnerException is KeyNotFoundException) + { + return AvaloniaProperty.UnsetValue; + } + } + + protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + { + return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName; + } + + protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; + } +} diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs new file mode 100644 index 0000000000..5c3295a9d8 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Reflection; +using System.Text; +using Avalonia.Data; +using Avalonia.Utilities; + +namespace Avalonia.Data.Core +{ + public abstract class IndexerNodeBase : SettableNode + { + private IDisposable _subscription; + + protected override void StartListeningCore(WeakReference reference) + { + var target = reference.Target; + var incc = target as INotifyCollectionChanged; + var inpc = target as INotifyPropertyChanged; + var inputs = new List>(); + + 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))); + } + + _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged); + } + + protected override void StopListeningCore() + { + _subscription.Dispose(); + } + + protected abstract object GetValue(object target); + + protected abstract int? TryGetFirstArgumentAsInt(); + + private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + { + if (sender is IList) + { + var index = TryGetFirstArgumentAsInt(); + + if (index == null) + { + return false; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count) || + (index >= e.OldStartingIndex && + index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + } + + return true; // Implementation defined meaning for the index, so just try to update anyway + } + + protected abstract bool ShouldUpdate(object sender, PropertyChangedEventArgs e); + } +} diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index f277005cec..20f1bcd21e 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -7,7 +7,7 @@ using Avalonia.Data; 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/ExpressionTreeParser.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs new file mode 100644 index 0000000000..db5d117687 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace Avalonia.Data.Core.Parsers +{ + static class ExpressionTreeParser + { + public static ExpressionNode Parse(Expression expr, bool enableDataValidation) + { + var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation); + + visitor.Visit(expr); + + var nodes = visitor.Nodes; + + for (int n = 0; n < nodes.Count - 1; ++n) + { + nodes[n].Next = nodes[n + 1]; + } + + return nodes.FirstOrDefault() ?? new EmptyExpressionNode(); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs new file mode 100644 index 0000000000..1b4d1c200d --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace Avalonia.Data.Core.Parsers +{ + class ExpressionVisitorNodeBuilder : ExpressionVisitor + { + private const string MultiDimensionalArrayGetterMethodName = "Get"; + private static PropertyInfo AvaloniaObjectIndexer; + private static MethodInfo CreateDelegateMethod; + + private readonly bool _enableDataValidation; + + static ExpressionVisitorNodeBuilder() + { + AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) }); + CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) }); + } + + public List Nodes { get; } + + public ExpressionVisitorNodeBuilder(bool enableDataValidation) + { + _enableDataValidation = enableDataValidation; + Nodes = new List(); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool)) + { + Nodes.Add(new LogicalNotNode()); + } + else if (node.NodeType == ExpressionType.Convert) + { + if (node.Operand.Type.IsAssignableFrom(node.Type)) + { + // Ignore inheritance casts + } + else + { + throw new ExpressionParseException(0, $"Cannot parse non-inheritance casts in a binding expression."); + } + } + else if (node.NodeType == ExpressionType.TypeAs) + { + // Ignore as operator. + } + else + { + throw new ExpressionParseException(0, $"Unable to parse unary operator {node.NodeType} in a binding expression"); + } + + return base.VisitUnary(node); + } + + protected override Expression VisitMember(MemberExpression node) + { + var visited = base.VisitMember(node); + Nodes.Add(new PropertyAccessorNode(node.Member.Name, _enableDataValidation)); + return visited; + } + + protected override Expression VisitIndex(IndexExpression node) + { + Visit(node.Object); + + if (node.Indexer == AvaloniaObjectIndexer) + { + var property = GetArgumentExpressionValue(node.Arguments[0]); + Nodes.Add(new AvaloniaPropertyAccessorNode(property, _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 Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right })); + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitBlock(BlockExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override CatchBlock VisitCatchBlock(CatchBlock node) + { + throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions."); + } + + protected override Expression VisitConditional(ConditionalExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitDynamic(DynamicExpression node) + { + throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions."); + } + + protected override ElementInit VisitElementInit(ElementInit node) + { + throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression."); + } + + protected override Expression VisitGoto(GotoExpression node) + { + throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions."); + } + + protected override Expression VisitInvocation(InvocationExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLabel(LabelExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitListInit(ListInitExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLoop(LoopExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) + { + throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions."); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method == CreateDelegateMethod) + { + var visited = Visit(node.Arguments[1]); + Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue(node.Object).Name, _enableDataValidation)); + return node; + } + else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`')) + { + if (node.Method.IsStatic) + { + Visit(node.Arguments[0]); + } + else + { + Visit(node.Object); + } + Nodes.Add(new StreamNode()); + return node; + } + + var property = TryGetPropertyFromMethod(node.Method); + + if (property != null) + { + return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); + } + else if (node.Object.Type.IsArray && node.Method.Name == MultiDimensionalArrayGetterMethodName) + { + return Visit(Expression.MakeIndex(node.Object, null, node.Arguments)); + } + + throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType.AssemblyQualifiedName}.{node.Method.Name}'."); + } + + private PropertyInfo TryGetPropertyFromMethod(MethodInfo method) + { + var type = method.DeclaringType; + return type.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method); + } + + protected override Expression VisitSwitch(SwitchExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTry(TryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTypeBinary(TypeBinaryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index a163c07f87..ee91f964ff 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -60,35 +60,7 @@ namespace Avalonia.Data.Core.Plugins private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName) { - 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) diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index e9831eb047..a916142675 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -8,7 +8,7 @@ using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { - internal class PropertyAccessorNode : SettableNode + public class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; private IPropertyAccessor _accessor; diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 092cdbe48f..e7c6ab766f 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; 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/Avalonia.Base/Data/Core/StreamBindingExtensions.cs b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs new file mode 100644 index 0000000000..fa8b56765c --- /dev/null +++ b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia +{ + public static class StreamBindingExtensions + { + internal static string StreamBindingName = "StreamBinding"; + + public static T StreamBinding(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/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 415def4d30..6fc178e7f8 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; namespace Avalonia.Data.Core { - internal class StreamNode : ExpressionNode + public class StreamNode : ExpressionNode { private IDisposable _subscription; diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs index fb0131a9b4..54234fe406 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs @@ -19,7 +19,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - 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 98203deebe..c3229d814c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -37,6 +37,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions 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 aa3c359953..fa91ab60ff 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 @@ namespace Avalonia.Markup.Xaml.Templates 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 a733ef761c..bd2b9d2efd 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.Controls.Templates; 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 @@ namespace Avalonia.Markup.Xaml.Templates { 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 ef5ebb9844..e5f7ea1742 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.LogicalTree; +using Avalonia.Markup.Parsers; using Avalonia.Reactive; using Avalonia.VisualTree; @@ -85,6 +86,11 @@ namespace Avalonia.Data 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, @@ -189,20 +195,22 @@ namespace Avalonia.Data if (!targetIsDataContext) { - var result = new ExpressionObserver( + var result = ExpressionObserverBuilder.Build( () => target.GetValue(StyledElement.DataContextProperty), path, new UpdateSignal(target, StyledElement.DataContextProperty), - enableDataValidation); + enableDataValidation, + typeResolver: TypeResolver); return result; } else { - return new ExpressionObserver( + return ExpressionObserverBuilder.Build( GetParentDataContext(target), path, - enableDataValidation); + enableDataValidation, + typeResolver: TypeResolver); } } @@ -215,11 +223,12 @@ namespace Avalonia.Data 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; } @@ -251,10 +260,11 @@ namespace Avalonia.Data throw new InvalidOperationException("Invalid tree to traverse."); } - return new ExpressionObserver( + return ExpressionObserverBuilder.Build( controlLocator, path, - enableDataValidation); + enableDataValidation, + typeResolver: TypeResolver); } private ExpressionObserver CreateSourceObserver( @@ -264,7 +274,7 @@ namespace Avalonia.Data { Contract.Requires(source != null); - return new ExpressionObserver(source, path, enableDataValidation); + return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver); } private ExpressionObserver CreateTemplatedParentObserver( @@ -273,12 +283,13 @@ namespace Avalonia.Data bool enableDataValidation) { Contract.Requires(target != null); - - var result = new ExpressionObserver( + + var result = ExpressionObserverBuilder.Build( () => target.GetValue(StyledElement.TemplatedParentProperty), path, new UpdateSignal(target, StyledElement.TemplatedParentProperty), - 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 17200a62b1..ae48657c01 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 0000000000..ddbe252fc0 --- /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 static 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 80% rename from src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs rename to src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 5c74c5cd13..95bb421777 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 @@ namespace Avalonia.Data.Core.Parsers 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,14 @@ namespace Avalonia.Data.Core.Parsers throw new ExpressionParseException(r.Position, "Expected ')'."); } - nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation)); + if (_typeResolver == null) + { + throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); + } + + var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns, owner), name); + + nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation)); return State.AfterMember; } @@ -157,7 +180,7 @@ namespace Avalonia.Data.Core.Parsers 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 b0a9ff4df2..f86f2db321 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 76% rename from src/Avalonia.Base/Data/Core/IndexerNode.cs rename to src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index afdfe90c4c..b3d0555f14 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -12,53 +12,19 @@ using System.Linq; 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 : SettableNode + internal class StringIndexerNode : IndexerNodeBase { - private IDisposable _subscription; - - public IndexerNode(IList arguments) + public StringIndexerNode(IList arguments) { Arguments = arguments; } public override string Description => "[" + string.Join(",", Arguments) + "]"; - protected override void StartListeningCore(WeakReference reference) - { - var target = reference.Target; - var incc = target as INotifyCollectionChanged; - var inpc = target as INotifyPropertyChanged; - var inputs = new List>(); - - 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))); - } - - _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged); - } - - protected override void StopListeningCore() - { - _subscription.Dispose(); - } - protected override bool SetTargetValueCore(object value, BindingPriority priority) { var typeInfo = Target.Target.GetType().GetTypeInfo(); @@ -163,7 +129,7 @@ namespace Avalonia.Data.Core 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; @@ -316,45 +282,19 @@ namespace Avalonia.Data.Core } } - private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) { - if (sender is IList) - { - object indexObject; - - if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject)) - { - return false; - } - - var index = (int)indexObject; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - return index >= e.NewStartingIndex; - case NotifyCollectionChangedAction.Remove: - return index >= e.OldStartingIndex; - case NotifyCollectionChangedAction.Replace: - return index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count; - case NotifyCollectionChangedAction.Move: - return (index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count) || - (index >= e.OldStartingIndex && - index < e.OldStartingIndex + e.OldItems.Count); - case NotifyCollectionChangedAction.Reset: - return true; - } - } - - return true; // Implementation defined meaning for the index, so just try to update anyway + var typeInfo = sender.GetType().GetTypeInfo(); + return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; } - private bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + protected override int? TryGetFirstArgumentAsInt() { - var typeInfo = sender.GetType().GetTypeInfo(); - return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; + if (TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out var value)) + { + return (int?)value; + } + return null; } } } 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 14187c769a..9355bc9aa3 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 e50056ddef..bb76387e61 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -52,11 +52,11 @@ namespace Avalonia.Markup.Parsers 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 81acd6a087..d51f56f558 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; 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 @@ namespace Avalonia.Base.UnitTests.Data.Core 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(ExpressionObserver.Create(data, o => o.StringValue), typeof(string)); var result = await target.Take(1); Assert.Equal("foo", result); @@ -34,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Set_Simple_Property_Value() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string)); target.OnNext("bar"); @@ -47,7 +48,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Set_Indexed_Value() { var data = new { Foo = new[] { "foo" } }; - var target = new BindingExpression(new ExpressionObserver(data, "Foo[0]"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.Foo[0]), typeof(string)); target.OnNext("bar"); @@ -60,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Convert_Get_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = new BindingExpression(new ExpressionObserver(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); @@ -72,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Getting_Invalid_Double_String_Should_Return_BindingError() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(new ExpressionObserver(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); @@ -84,7 +85,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() { var data = new Class1 { StringValue = null }; - var target = new BindingExpression(new ExpressionObserver(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); @@ -96,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Convert_Set_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); target.OnNext(6.7); @@ -109,7 +110,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Convert_Get_Double_To_String() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(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); @@ -121,7 +122,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Convert_Set_Double_To_String() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext($"{6.7}"); @@ -135,7 +136,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - new ExpressionObserver(data, "StringValue"), + ExpressionObserver.Create(data, o => o.StringValue), typeof(int), 42, DefaultValueConverter.Instance); @@ -156,7 +157,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - new ExpressionObserver(data, "StringValue", true), + ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), 42, DefaultValueConverter.Instance); @@ -177,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - new ExpressionObserver(data, "StringValue"), + ExpressionObserver.Create(data, o => o.StringValue), typeof(int), "bar", DefaultValueConverter.Instance); @@ -199,7 +200,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - new ExpressionObserver(data, "StringValue", true), + ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), "bar", DefaultValueConverter.Instance); @@ -220,7 +221,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Setting_Invalid_Double_String_Should_Not_Change_Target() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext("foo"); @@ -234,7 +235,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { DoubleValue = 5.6 }; var target = new BindingExpression( - new ExpressionObserver(data, "DoubleValue"), + ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), "9.8", DefaultValueConverter.Instance); @@ -250,7 +251,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Coerce_Setting_Null_Double_To_Default_Value() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext(null); @@ -263,7 +264,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext(AvaloniaProperty.UnsetValue); @@ -279,7 +280,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var converter = new Mock(); var target = new BindingExpression( - new ExpressionObserver(data, "DoubleValue"), + ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), converter.Object, converterParameter: "foo"); @@ -297,7 +298,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); var target = new BindingExpression( - new ExpressionObserver(data, "DoubleValue"), + ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), converter.Object, converterParameter: "foo"); @@ -314,7 +315,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { 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(ExpressionObserver.Create(data, o => o.DoubleValue, true), typeof(string)); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -341,7 +342,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Second_Subscription_Should_Fire_Immediately() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string)); object result = null; target.Subscribe(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs index 3ed2c0b7eb..7e47e9b1eb 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs @@ -13,16 +13,12 @@ namespace Avalonia.Base.UnitTests.Data.Core { public class ExpressionObserverTests_AttachedProperty { - public ExpressionObserverTests_AttachedProperty() - { - var foo = Owner.FooProperty; - } [Fact] public async Task Should_Get_Attached_Property_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "(Owner.Foo)"); + var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]); var result = await target.Take(1); Assert.Equal("foo", result); @@ -41,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } }; - var target = new ExpressionObserver(data, "Next.(Owner.Foo)"); + var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -53,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Simple_Attached_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "(Owner.Foo)"); + var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -77,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } }; - var target = new ExpressionObserver(data, "Next.(Owner.Foo)"); + var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -96,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core Func> run = () => { var source = new Class1(); - var target = new ExpressionObserver(source, "(Owner.Foo)"); + var target = ExpressionObserver.Create(source, o => o.Next[Owner.FooProperty]); return Tuple.Create(target, new WeakReference(source)); }; @@ -108,22 +104,6 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Null(result.Item2.Target); } - [Fact] - public void Should_Fail_With_Attached_Property_With_Only_1_Part() - { - var data = new Class1(); - - Assert.Throws(() => new ExpressionObserver(data, "(Owner)")); - } - - [Fact] - public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts() - { - var data = new Class1(); - - Assert.Throws(() => new ExpressionObserver(data, "(Owner.Foo.Bar)")); - } - 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 bf2b6cbcb2..8d5510dd20 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 System.Threading.Tasks; using Avalonia.Diagnostics; using Avalonia.Data.Core; using Xunit; +using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { @@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Equal("foo", result); @@ -34,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_ClrProperty_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "ClrProperty"); + var target = ExpressionObserver.Create(data, o => o.ClrProperty); var result = await target.Take(1); Assert.Equal("clr-property", result); @@ -44,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Simple_Property_Value() { var data = new Class1(); - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -63,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Data.Core Func> run = () => { var source = new Class1(); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); return Tuple.Create(target, new WeakReference(source)); }; @@ -80,6 +81,8 @@ namespace Avalonia.Base.UnitTests.Data.Core 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 3732569753..b66dd610dd 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.Linq; using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Xunit; @@ -19,7 +20,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() { var data = new ExceptionTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); var validationMessageFound = false; observer.OfType() @@ -36,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Exception_Validation_Sends_DataValidationError() { var data = new ExceptionTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var validationMessageFound = false; observer.OfType() @@ -53,7 +54,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); observer.Subscribe(_ => { }); @@ -64,7 +65,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Enabled_Indei_Validation_Subscribes() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var sub = observer.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); @@ -76,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Validation_Plugins_Send_Correct_Notifications() { var data = new IndeiTest(); - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var result = new List(); var errmsg = string.Empty; @@ -122,10 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core Inner = new IndeiTest() }; - var observer = new ExpressionObserver( - data, - $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", - true); + var observer = ExpressionObserver.Create(data, o => o.Inner.MustBePositive, true); observer.Subscribe(_ => { }); @@ -133,19 +131,16 @@ namespace Avalonia.Base.UnitTests.Data.Core // intermediate object in a chain so for the moment I'm not sure what the result of // 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 = new ExpressionObserver( - 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)); @@ -153,13 +148,12 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { new BindingNotification( - new MarkupBindingChainException("Null value", "Inner.MustBePositive", "Inner"), + new MarkupBindingChainException("Null value", "o => o.Inner.MustBePositive", "Inner"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, result); GC.KeepAlive(container); - GC.KeepAlive(inner); } public class ExceptionTest : NotifyingBase @@ -220,9 +214,9 @@ namespace Avalonia.Base.UnitTests.Data.Core private class Container : IndeiBase { - private object _inner; + private IndeiTest _inner; - public object Inner + public IndeiTest Inner { get { return _inner; } set { _inner = value; RaisePropertyChanged(); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs new file mode 100644 index 0000000000..9b587d7679 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Data.Core; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Base.UnitTests.Data.Core +{ + public class ExpressionObserverTests_ExpressionTree + { + [Fact] + public async Task IdentityExpression_Creates_IdentityObserver() + { + var target = new object(); + + var observer = ExpressionObserver.Create(target, o => o); + + Assert.Equal(target, await observer.Take(1)); + GC.KeepAlive(target); + } + + [Fact] + public async Task Property_Access_Expression_Observes_Property() + { + var target = new Class1(); + + var observer = ExpressionObserver.Create(target, o => o.Foo); + + Assert.Null(await observer.Take(1)); + + using (observer.Subscribe(_ => {})) + { + target.Foo = "Test"; + } + + Assert.Equal("Test", await observer.Take(1)); + + GC.KeepAlive(target); + } + + [Fact] + public void Property_Acccess_Expression_Can_Set_Property() + { + var data = new Class1(); + var target = ExpressionObserver.Create(data, o => o.Foo); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue("baz")); + } + + GC.KeepAlive(data); + } + + [Fact] + public async Task Indexer_Accessor_Can_Read_Value() + { + var data = new[] { 1, 2, 3, 4 }; + + var target = ExpressionObserver.Create(data, o => o[0]); + + Assert.Equal(data[0], await target.Take(1)); + GC.KeepAlive(data); + } + + [Fact] + public async Task Indexer_List_Accessor_Can_Read_Value() + { + var data = new List { 1, 2, 3, 4 }; + + var target = ExpressionObserver.Create(data, o => o[0]); + + Assert.Equal(data[0], await target.Take(1)); + GC.KeepAlive(data); + } + + [Fact] + public async Task Indexer_Accessor_Can_Read_Complex_Index() + { + var data = new Dictionary(); + + var key = new object(); + + data.Add(key, new object()); + + var target = ExpressionObserver.Create(data, o => o[key]); + + Assert.Equal(data[key], await target.Take(1)); + + GC.KeepAlive(data); + } + + [Fact] + public void Indexer_Can_Set_Value() + { + var data = new[] { 1, 2, 3, 4 }; + + var target = ExpressionObserver.Create(data, o => o[0]); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue(2)); + } + + GC.KeepAlive(data); + } + + [Fact] + public async Task Inheritance_Casts_Should_Be_Ignored() + { + NotifyingBase test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.Create(test, o => ((Class1)o).Foo); + + Assert.Equal("Test", await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public void Convert_Casts_Should_Error() + { + var test = 1; + + Assert.Throws(() => ExpressionObserver.Create(test, o => (double)o)); + } + + [Fact] + public async Task As_Operator_Should_Be_Ignored() + { + NotifyingBase test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.Create(test, o => (o as Class1).Foo); + + Assert.Equal("Test", await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public async Task Avalonia_Property_Indexer_Reads_Avalonia_Property_Value() + { + var test = new Class2(); + + var target = ExpressionObserver.Create(test, o => o[Class2.FooProperty]); + + Assert.Equal("foo", await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public async Task Complex_Expression_Correctly_Parsed() + { + var test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.Create(test, o => o.Foo.Length); + + Assert.Equal(test.Foo.Length, await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public void Should_Get_Completed_Task_Value() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var data = new { Foo = Task.FromResult("foo") }; + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + + Assert.Equal(new[] { "foo" }, result); + + GC.KeepAlive(data); + } + } + + [Fact] + public async Task Should_Create_Method_Binding() + { + var data = new Class3(); + var target = ExpressionObserver.Create(data, o => (Action)o.Method); + var value = await target.Take(1); + + Assert.IsAssignableFrom(value); + GC.KeepAlive(data); + } + + private class Class1 : NotifyingBase + { + private string _foo; + + public string Foo + { + get { return _foo; } + set + { + _foo = value; + RaisePropertyChanged(nameof(Foo)); + } + } + } + + + private class Class2 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", defaultValue: "foo"); + + 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 8a54f968b1..cbbb5f4715 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.Diagnostics; using Avalonia.Data.Core; using Avalonia.UnitTests; using Xunit; +using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { @@ -20,7 +21,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Array_Value() { var data = new { Foo = new [] { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, x => x.Foo[1]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -28,47 +29,11 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public async Task Should_Get_UnsetValue_For_Invalid_Array_Index() - { - var data = new { Foo = new[] { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[invalid]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index() - { - var data = new { Foo = new Dictionary { { 1, "foo" } } }; - var target = new ExpressionObserver(data, "Foo[invalid]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Get_UnsetValue_For_Object_Without_Indexer() - { - var data = new { Foo = 5 }; - var target = new ExpressionObserver(data, "Foo[noindexer]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - [Fact] 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 = ExpressionObserver.Create(data, o => o.Foo[1, 1]); var result = await target.Take(1); Assert.Equal("qux", result); @@ -80,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core 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 = ExpressionObserver.Create(data, o => o.Foo["foo"]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -92,7 +57,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Value_For_Non_String_Indexer() { var data = new { Foo = new Dictionary { { 1.0, "bar" }, { 2.0, "qux" } } }; - var target = new ExpressionObserver(data, "Foo[1.0]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1.0]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -104,19 +69,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[2]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue() - { - var data = new { Foo = new[] { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1,2]"); + var target = ExpressionObserver.Create(data, o => o.Foo[2]); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -128,7 +81,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new List { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[2]"); + var target = ExpressionObserver.Create(data, o => o.Foo[2]); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -140,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_List_Value() { var data = new { Foo = new List { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -152,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_INCC_Add() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(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))) @@ -170,7 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_INCC_Remove() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(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))) @@ -188,7 +141,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_INCC_Replace() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(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))) @@ -209,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core // method, but even if it did we need to test with ObservableCollection as well // as AvaloniaList as it implements PropertyChanged as an explicit interface event. var data = new { Foo = new ObservableCollection { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -225,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_INCC_Reset() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -244,7 +197,7 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = new ExpressionObserver(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))) @@ -263,7 +216,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_SetArrayIndex() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = new ExpressionObserver(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); using (target.Subscribe(_ => { })) { @@ -285,8 +238,8 @@ namespace Avalonia.Base.UnitTests.Data.Core {"foo", 1 } } }; - - var target = new ExpressionObserver(data, "Foo[foo]"); + + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); using (target.Subscribe(_ => { })) { Assert.True(target.SetValue(4)); @@ -307,8 +260,8 @@ namespace Avalonia.Base.UnitTests.Data.Core {"foo", 1 } } }; - - var target = new ExpressionObserver(data, "Foo[bar]"); + + var target = ExpressionObserver.Create(data, o => o.Foo["bar"]); using (target.Subscribe(_ => { })) { Assert.True(target.SetValue(4)); @@ -326,7 +279,7 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = new ExpressionObserver(data, "Foo[foo]"); + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); using (target.Subscribe(_ => { })) { @@ -343,7 +296,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new[] { 1, 2, 3 }; - var target = new ExpressionObserver(data, "[1]"); + var target = ExpressionObserver.Create(data, o => o[1]); var value = await target.Take(1); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs index b88bf2c427..cf151f6244 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 System.Reactive.Subjects; using Microsoft.Reactive.Testing; using Avalonia.Data.Core; using Xunit; +using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { @@ -18,7 +19,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Complete_When_Source_Observable_Completes() { var source = new BehaviorSubject(1); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -31,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Complete_When_Source_Observable_Errors() { var source = new BehaviorSubject(1); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -44,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Complete_When_Update_Observable_Completes() { var update = new Subject(); - var target = new ExpressionObserver(() => 1, "Foo", update); + var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -57,7 +58,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Complete_When_Update_Observable_Errors() { var update = new Subject(); - var target = new ExpressionObserver(() => 1, "Foo", update); + var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -72,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var scheduler = new TestScheduler(); var source = scheduler.CreateColdObservable( OnNext(1, new { Foo = "foo" })); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -91,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var scheduler = new TestScheduler(); var update = scheduler.CreateColdObservable(); var data = new { Foo = "foo" }; - var target = new ExpressionObserver(() => data, "Foo", update); + var target = ExpressionObserver.Create(() => data, o => o.Foo, update); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -106,9 +107,9 @@ namespace Avalonia.Base.UnitTests.Data.Core 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 556352f6ca..54d7e98903 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.Reactive.Linq; 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 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Negate_Boolean_Value() { var data = new { Foo = true }; - var target = new ExpressionObserver(data, "!Foo"); + var target = ExpressionObserver.Create(data, o => !o.Foo); var result = await target.Take(1); Assert.False((bool)result); @@ -24,103 +25,11 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public async Task Should_Negate_0() - { - var data = new { Foo = 0 }; - var target = new ExpressionObserver(data, "!Foo"); - var result = await target.Take(1); - - Assert.True((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Negate_1() - { - var data = new { Foo = 1 }; - var target = new ExpressionObserver(data, "!Foo"); - var result = await target.Take(1); - - Assert.False((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Negate_False_String() - { - var data = new { Foo = "false" }; - var target = new ExpressionObserver(data, "!Foo"); - var result = await target.Take(1); - - Assert.True((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Negate_True_String() - { - var data = new { Foo = "True" }; - var target = new ExpressionObserver(data, "!Foo"); - var result = await target.Take(1); - - Assert.False((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() - { - var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "!Foo"); - var result = await target.Take(1); - - Assert.Equal( - new BindingNotification( - new InvalidCastException($"Unable to convert 'foo' to bool."), - BindingErrorType.Error), - result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() - { - var data = new { Foo = new object() }; - var target = new ExpressionObserver(data, "!Foo"); - var result = await target.Take(1); - - Assert.Equal( - new BindingNotification( - new InvalidCastException($"Unable to convert 'System.Object' to bool."), - BindingErrorType.Error), - result); - - GC.KeepAlive(data); - } - - [Fact] - public void SetValue_Should_Return_False_For_Invalid_Value() - { - var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "!Foo"); - target.Subscribe(_ => { }); - - Assert.False(target.SetValue("bar")); - - GC.KeepAlive(data); - } - [Fact] public void Can_SetValue_For_Valid_Value() { var data = new Test { Foo = true }; - var target = new ExpressionObserver(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 f1c39617eb..701fdbce9c 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.Linq; using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Xunit; @@ -15,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 = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = new ExpressionObserver(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 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new Class1(); - var target = new ExpressionObserver(data, "Next^.Foo"); + var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -83,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = new ExpressionObserver(data, "Foo^", true); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -105,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data1 = new Class1(); var data2 = new Class2("foo"); - var target = new ExpressionObserver(data1, "Next^.Foo", true); + var target = ExpressionObserver.Create(data1, o => o.Next.StreamBinding().Foo, true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -127,8 +128,8 @@ namespace Avalonia.Base.UnitTests.Data.Core { using (var sync = UnitTestSynchronizationContext.Begin()) { - var data = new Class2("foo"); - var target = new ExpressionObserver(data, "Foo^", true); + var data = new NotStreamable(); + var target = ExpressionObserver.Create(data, o => o.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -138,7 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core new[] { new BindingNotification( - new MarkupBindingChainException("Stream operator applied to unsupported type", "Foo^", "Foo^"), + new MarkupBindingChainException("Stream operator applied to unsupported type", "o => o.StreamBinding()", "^"), BindingErrorType.Error) }, result); @@ -163,5 +164,10 @@ namespace Avalonia.Base.UnitTests.Data.Core 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 a3cb11114a..c90683959e 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.Data.Core; using Avalonia.UnitTests; using Xunit; using System.Threading.Tasks; +using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { @@ -21,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value() { var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Equal("foo", result); @@ -33,7 +34,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Get_Simple_Property_Value_Type() { var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); target.Subscribe(_ => { }); @@ -46,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value_Null() { var data = new { Foo = (string)null }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Null(result); @@ -58,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_From_Base_Class() { var data = new Class3 { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Equal("foo", result); @@ -69,76 +70,65 @@ namespace Avalonia.Base.UnitTests.Data.Core [Fact] public async Task Should_Return_BindingNotification_Error_For_Root_Null() { - var data = new Class3 { Foo = "foo" }; - var target = new ExpressionObserver(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 = new ExpressionObserver(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 = new ExpressionObserver(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 = new ExpressionObserver(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 = new ExpressionObserver(data, "Foo.Bar.Baz"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz); var result = await target.Take(1); Assert.Equal("baz", result); @@ -150,7 +140,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Get_Simple_Property_Chain_Type() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; - var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz); target.Subscribe(_ => { }); @@ -159,28 +149,11 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public async Task Should_Return_BindingNotification_Error_For_Broken_Chain() - { - var data = new { Foo = new { Bar = 1 } }; - var target = new ExpressionObserver(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 = new ExpressionObserver(data, "Foo.Bar.Baz"); + var data = new { Foo = default(Class1) }; + var target = ExpressionObserver.Create(data, o => o.Foo.Foo.Length); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -189,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core new[] { new BindingNotification( - new MarkupBindingChainException("Null value", "Foo.Bar.Baz", "Foo"), + new MarkupBindingChainException("Null value", "o => o.Foo.Foo.Length", "Foo"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, @@ -198,22 +171,11 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public void Should_Have_Null_ResultType_For_Broken_Chain() - { - var data = new { Foo = new { Bar = 1 } }; - var target = new ExpressionObserver(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 = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -232,7 +194,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String() { var data = new Class1 { Bar = "foo" }; - var target = new ExpressionObserver(data, "Bar"); + var target = ExpressionObserver.Create(data, o => o.Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -262,7 +224,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_End_Of_Property_Chain_Changing() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -283,7 +245,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Property_Chain_Changing() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -316,7 +278,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } }; - var target = new ExpressionObserver(data, "Next.Next.Bar"); + var target = ExpressionObserver.Create(data, o => ((o.Next as Class2).Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -329,7 +291,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { "bar", new BindingNotification( - new MarkupBindingChainException("Null value", "Next.Next.Bar", "Next.Next"), + new MarkupBindingChainException("Null value", "o => ((o.Next As Class2).Next As Class2).Bar", "Next.Next"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), "bar" @@ -349,7 +311,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(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)); @@ -384,7 +346,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { Foo = "foo" }; var update = new Subject(); - var target = new ExpressionObserver(() => data.Foo, "", update); + var target = ExpressionObserver.Create(() => data.Foo, o => o, update); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -404,7 +366,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var source = scheduler.CreateColdObservable( OnNext(1, new Class1 { Foo = "foo" }), OnNext(2, new Class1 { Foo = "bar" })); - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -420,7 +382,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Subscribing_Multiple_Times_Should_Return_Values_To_All() { var data = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result1 = new List(); var result2 = new List(); var result3 = new List(); @@ -443,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once() { var data = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var sub1 = target.Subscribe(x => { }); var sub2 = target.Subscribe(x => { }); @@ -462,7 +424,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Set_Simple_Property_Value() { var data = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); using (target.Subscribe(_ => { })) { @@ -478,7 +440,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Set_Property_At_The_End_Of_Chain() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); using (target.Subscribe(_ => { })) { @@ -494,7 +456,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Return_False_For_Missing_Property() { var data = new Class1 { Next = new WithoutBar() }; - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); using (target.Subscribe(_ => { })) { @@ -508,7 +470,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Notify_New_Value_With_Inpc() { var data = new Class1(); - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -523,7 +485,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Notify_New_Value_Without_Inpc() { var data = new Class1(); - var target = new ExpressionObserver(data, "Bar"); + var target = ExpressionObserver.Create(data, o => o.Bar); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -538,7 +500,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Return_False_For_Missing_Object() { var data = new Class1(); - var target = new ExpressionObserver(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); using (target.Subscribe(_ => { })) { @@ -555,7 +517,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var second = new Class1 { Foo = "bar" }; var root = first; var update = new Subject(); - var target = new ExpressionObserver(() => root, "Foo", update); + var target = ExpressionObserver.Create(() => root, o => o.Foo, update); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -570,7 +532,7 @@ namespace Avalonia.Base.UnitTests.Data.Core "foo", "bar", new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue) }, @@ -589,7 +551,7 @@ namespace Avalonia.Base.UnitTests.Data.Core Func> run = () => { var source = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); return Tuple.Create(target, new WeakReference(source)); }; @@ -673,9 +635,9 @@ namespace Avalonia.Base.UnitTests.Data.Core { } - 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 a163229e26..99507a2c07 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; 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 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Set_Simple_Property_Value() { var data = new { Foo = "foo" }; - var target = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); using (target.Subscribe(_ => { })) { @@ -30,7 +31,8 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Set_Value_On_Simple_Property_Chain() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Foo.Bar"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar); + using (target.Subscribe(_ => { })) { @@ -44,14 +46,15 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Not_Try_To_Set_Value_On_Broken_Chain() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(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")); } /// @@ -67,13 +70,15 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; var rootObservable = new BehaviorSubject(data); - var target = new ExpressionObserver(rootObservable, "Foo.Bar"); + var target = ExpressionObserver.Create(rootObservable, o => o.Foo.Bar); - target.Subscribe(_ => { }); - rootObservable.OnNext(null); - target.SetValue("baz"); + using (target.Subscribe(_ => { })) + { + rootObservable.OnNext(null); + target.SetValue("baz"); + Assert.Equal("bar", data.Foo.Bar); + } - Assert.Equal("bar", data.Foo.Bar); } private class Class1 : NotifyingBase diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs index 3b9a23f846..9ea0a5e3e1 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.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; using Avalonia.UnitTests; using Xunit; @@ -15,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 = new ExpressionObserver(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = Task.FromResult("foo") }; - var target = new ExpressionObserver(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -59,7 +60,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var tcs = new TaskCompletionSource(); var data = new Class1(tcs.Task); - var target = new ExpressionObserver(data, "Next^.Foo"); + var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -79,7 +80,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = new ExpressionObserver(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -105,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = TaskFromException(new NotSupportedException()) }; - var target = new ExpressionObserver(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -130,7 +131,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = new ExpressionObserver(data, "Foo^", true); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true); var result = new List(); 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 a7263cacbd..c49c343a45 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -515,7 +515,7 @@ namespace Avalonia.Controls.UnitTests public InstancedBinding ItemsSelector(object item) { - var obs = new ExpressionObserver(item, nameof(Node.Children)); + var obs = ExpressionObserver.Create(item, o => (o as Node).Children); return InstancedBinding.OneWay(obs); } diff --git a/tests/Avalonia.LeakTests/ExpressionObserverTests.cs b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs index 96f9e37897..d227696545 100644 --- a/tests/Avalonia.LeakTests/ExpressionObserverTests.cs +++ b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs @@ -24,7 +24,7 @@ namespace Avalonia.LeakTests Func run = () => { var source = new { Foo = new AvaloniaList {"foo", "bar"} }; - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); target.Subscribe(_ => { }); return target; @@ -42,7 +42,7 @@ namespace Avalonia.LeakTests Func run = () => { var source = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(source, "Foo", true); + var target = ExpressionObserver.Create(source, o => o.Foo, true); target.Subscribe(_ => { }); return target; @@ -60,7 +60,7 @@ namespace Avalonia.LeakTests Func run = () => { var source = new { Foo = new NonIntegerIndexer() }; - var target = new ExpressionObserver(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); target.Subscribe(_ => { }); return target; @@ -78,7 +78,7 @@ namespace Avalonia.LeakTests Func run = () => { var source = new { Foo = new MethodBound() }; - var target = new ExpressionObserver(source, "Foo.A"); + var target = ExpressionObserver.Create(source, o => (Action)o.Foo.A); target.Subscribe(_ => { }); return target; }; diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs similarity index 71% rename from tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests.cs rename to tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs index 146b7cace1..212b16965c 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 @@ namespace Avalonia.Base.UnitTests.Data.Core [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 @@ namespace Avalonia.Base.UnitTests.Data.Core [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 @@ namespace Avalonia.Base.UnitTests.Data.Core [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 @@ namespace Avalonia.Base.UnitTests.Data.Core [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 @@ namespace Avalonia.Base.UnitTests.Data.Core [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 @@ namespace Avalonia.Base.UnitTests.Data.Core [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 @@ namespace Avalonia.Base.UnitTests.Data.Core [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 @@ namespace Avalonia.Base.UnitTests.Data.Core [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 @@ namespace Avalonia.Base.UnitTests.Data.Core [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"); @@ -135,6 +137,15 @@ namespace Avalonia.Base.UnitTests.Data.Core 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); @@ -145,9 +156,9 @@ namespace Avalonia.Base.UnitTests.Data.Core 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 1bf1ce132a..347fc0a744 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/ExpressionObserverBuilderTests_AttachedProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs new file mode 100644 index 0000000000..5b97ab7ae6 --- /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 0000000000..816185cb64 --- /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 0000000000..39d6152b69 --- /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 83% rename from tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs rename to tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs index ef89c2b4bd..b0623aa456 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_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; @@ -8,9 +9,9 @@ using System.Text; using System.Threading.Tasks; using Xunit; -namespace Avalonia.Base.UnitTests.Data.Core +namespace Avalonia.Markup.UnitTests.Parsers { - public class ExpressionObserverTests_Method + public class ExpressionObserverBuilderTests_Method { private class TestObject { @@ -30,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Data.Core 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 @@ namespace Avalonia.Base.UnitTests.Data.Core 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 @@ namespace Avalonia.Base.UnitTests.Data.Core 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 @@ namespace Avalonia.Base.UnitTests.Data.Core 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.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs new file mode 100644 index 0000000000..24f6407908 --- /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 0000000000..a97c998264 --- /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); + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 8cb2639125..62a9e80585 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -6,7 +6,7 @@ using Avalonia.Markup.Parsers; 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 360be7f909..f5a08b6d70 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs @@ -3,7 +3,7 @@ using Avalonia.Controls; using Avalonia.Markup.Parsers; using Xunit; -namespace Avalonia.Markup.Xaml.UnitTests.Parsers +namespace Avalonia.Markup.UnitTests.Parsers { public class SelectorParserTests {