Browse Source

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

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

60
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

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

2
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 => ".";
}

2
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 =

30
src/Avalonia.Base/Data/Core/ExpressionNodeBuilder.cs

@ -1,30 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data.Core.Parsers;
namespace Avalonia.Data.Core
{
internal static class ExpressionNodeBuilder
{
public static ExpressionNode Build(string expression, bool enableValidation = false)
{
if (string.IsNullOrWhiteSpace(expression))
{
throw new ArgumentException("'expression' may not be empty.");
}
var reader = new Reader(expression);
var parser = new ExpressionParser(enableValidation);
var node = parser.Parse(reader);
if (!reader.End)
{
throw new ExpressionParseException(reader.Position, "Expected end of expression.");
}
return node;
}
}
}

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

4
src/Avalonia.Base/Data/Core/ExpressionParseException.cs

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

71
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

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

92
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

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

2
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 => "!";

27
src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs

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

219
src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs

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

30
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)

2
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;

2
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)
{

27
src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia
{
public static class StreamBindingExtensions
{
internal static string StreamBindingName = "StreamBinding";
public static T StreamBinding<T>(this Task<T> @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
public static object StreamBinding(this Task @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
public static T StreamBinding<T>(this IObservable<T> @this)
{
throw new InvalidOperationException("This should be used only in a binding expression");
}
}
}

2
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;

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

1
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,

3
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);

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

35
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; }
/// <summary>
/// Gets or sets a function used to resolve types from names in the binding path.
/// </summary>
public Func<string, string, Type> TypeResolver { get; set; }
/// <inheritdoc/>
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<ArgumentNullException>(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<ArgumentNullException>(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<ArgumentNullException>(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;
}

3
src/Avalonia.Base/Data/Core/Parsers/ArgumentListParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@ -1,11 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// 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
{

75
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs

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

35
src/Avalonia.Base/Data/Core/Parsers/ExpressionParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@ -1,18 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// 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<string, string, Type> _typeResolver;
public ExpressionParser(bool enableValidation)
public ExpressionParser(bool enableValidation, Func<string, string, Type> typeResolver)
{
_typeResolver = typeResolver;
_enableValidation = enableValidation;
}
@ -130,7 +134,19 @@ namespace Avalonia.Data.Core.Parsers
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
{
var owner = IdentifierParser.Parse(r);
string ns = string.Empty;
string owner;
var ownerOrNamespace = IdentifierParser.Parse(r);
if (r.TakeIf(':'))
{
ns = ownerOrNamespace;
owner = IdentifierParser.Parse(r);
}
else
{
owner = ownerOrNamespace;
}
if (r.End || !r.TakeIf('.'))
{
@ -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;
}

2
src/Avalonia.Base/Data/Core/Parsers/IdentifierParser.cs → src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs

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

88
src/Avalonia.Base/Data/Core/IndexerNode.cs → src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs

@ -12,53 +12,19 @@ using System.Linq;
using System.Reflection;
using System.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<string> arguments)
public StringIndexerNode(IList<string> 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<IObservable<object>>();
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
if (inpc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
_subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
{
_subscription.Dispose();
}
protected override bool SetTargetValueCore(object value, BindingPriority priority)
{
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;
}
}
}

2
src/Avalonia.Base/Data/Core/Parsers/Reader.cs → 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
{

4
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)
{

43
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<BindingNotification>(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<IValueConverter>();
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<IValueConverter>();
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<IValueConverter>();
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string));
var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue, true), typeof(string));
var result = new List<object>();
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();

30
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<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -77,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
}
};
var target = new ExpressionObserver(data, "Next.(Owner.Foo)");
var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -96,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Func<Tuple<ExpressionObserver, WeakReference>> 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<ExpressionParseException>(() => new ExpressionObserver(data, "(Owner)"));
}
[Fact]
public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts()
{
var data = new Class1();
Assert.Throws<ExpressionParseException>(() => new ExpressionObserver(data, "(Owner.Foo.Bar)"));
}
private static class Owner
{
public static readonly AttachedProperty<string> FooProperty =

11
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<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -63,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Func<Tuple<ExpressionObserver, WeakReference>> 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<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", defaultValue: "foo");
public string Foo { get => GetValue(FooProperty); set => SetValue(FooProperty, value); }
public string ClrProperty { get; } = "clr-property";
}
}

32
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<BindingNotification>()
@ -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<BindingNotification>()
@ -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<object>();
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<object>();
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(); }

224
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs

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

89
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<int, string> { { 1, "foo" } } };
var target = new ExpressionObserver(data, "Foo[invalid]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_UnsetValue_For_Object_Without_Indexer()
{
var data = new { Foo = 5 };
var target = new ExpressionObserver(data, "Foo[noindexer]");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
GC.KeepAlive(data);
}
[Fact]
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<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
var target = new ExpressionObserver(data, "Foo[foo]");
var target = ExpressionObserver.Create(data, o => o.Foo["foo"]);
var result = await target.Take(1);
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<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
var target = new ExpressionObserver(data, "Foo[1.0]");
var target = ExpressionObserver.Create(data, o => o.Foo[1.0]);
var result = await target.Take(1);
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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]");
var target = ExpressionObserver.Create(data, o => o.Foo[2]);
var result = await target.Take(1);
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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = await target.Take(1);
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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]");
var target = ExpressionObserver.Create(data, o => o.Foo[2]);
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
@ -170,7 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Remove()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[0]");
var target = ExpressionObserver.Create(data, o => o.Foo[0]);
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
@ -188,7 +141,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_INCC_Replace()
{
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x)))
@ -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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = new List<object>();
var 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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var target = ExpressionObserver.Create(data, o => o.Foo[1]);
var result = new List<object>();
var 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<object>();
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);

17
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<object>(1);
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create<object, object>(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<object>(1);
var target = new ExpressionObserver(source, "Foo");
var target = ExpressionObserver.Create<object, object>(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<Unit>();
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<Unit>();
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<object>();
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<Unit>();
var data = new { Foo = "foo" };
var target = new ExpressionObserver(() => data, "Foo", update);
var target = ExpressionObserver.Create(() => data, o => o.Foo, update);
var result = new List<object>();
using (target.Subscribe(x => result.Add(x)))
@ -106,9 +107,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
private Recorded<Notification<object>> OnNext(long time, object value)
private Recorded<Notification<T>> OnNext<T>(long time, T value)
{
return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value));
return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
}
}
}

97
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));

24
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<string>("foo");
var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var source = new BehaviorSubject<string>("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<object>();
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<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -83,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo^", true);
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
var result = new List<object>();
var 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<object>();
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<object>();
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(); }
}
}
}

120
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<object, string>(Observable.Return(AvaloniaProperty.UnsetValue), o => (o as Class3).Foo);
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new MarkupBindingChainException("Null value", "Foo", string.Empty),
new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Get_Simple_Property_Chain()
{
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = 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<BindingNotification>(result);
Assert.Equal(
new BindingNotification(
new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
{
var data = new { Foo = default(object) };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var data = new { Foo = default(Class1) };
var target = ExpressionObserver.Create(data, o => o.Foo.Foo.Length);
var result = new List<object>();
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<object>();
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<object>();
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<object>();
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<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -316,7 +278,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
}
};
var target = new ExpressionObserver(data, "Next.Next.Bar");
var target = ExpressionObserver.Create(data, o => ((o.Next as Class2).Next as Class2).Bar);
var result = new List<object>();
var 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<object>();
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<Unit>();
var target = new ExpressionObserver(() => data.Foo, "", update);
var target = ExpressionObserver.Create(() => data.Foo, o => o, update);
var result = new List<object>();
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<object>();
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<object>();
var result2 = new List<object>();
var result3 = new List<object>();
@ -443,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
{
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<object>();
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<object>();
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<Unit>();
var target = new ExpressionObserver(() => root, "Foo", update);
var target = ExpressionObserver.Create(() => root, o => o.Foo, update);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -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<Tuple<ExpressionObserver, WeakReference>> 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<Notification<object>> OnNext(long time, object value)
private Recorded<Notification<T>> OnNext<T>(long time, T value)
{
return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value));
return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
}
}
}

29
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<string>().Subscribe(x => { });
data.Foo = null;
using (target.OfType<string>().Subscribe(x => { }))
{
data.Foo = null;
Assert.False(target.SetValue("foo"));
}
Assert.False(target.SetValue("foo"));
}
/// <summary>
@ -67,13 +70,15 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var rootObservable = new BehaviorSubject<Class1>(data);
var target = 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

15
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<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo");
var target = ExpressionObserver.Create(data, o => o.Foo);
var result = new List<object>();
var 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<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -59,7 +60,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var tcs = new TaskCompletionSource<Class2>();
var data = new Class1(tcs.Task);
var target = new ExpressionObserver(data, "Next^.Foo");
var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -79,7 +80,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo^");
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>();
var 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<object>();
var sub = target.Subscribe(x => result.Add(x));
@ -130,7 +131,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo^", true);
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));

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

8
tests/Avalonia.LeakTests/ExpressionObserverTests.cs

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

47
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs

@ -4,16 +4,18 @@
using System.Collections.Generic;
using System.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<LogicalNotNode>(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<LogicalNotNode>(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<IndexerNode>(result[1]);
Assert.IsType<StringIndexerNode>(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<IndexerNode>(result[1]);
Assert.IsType<StringIndexerNode>(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<StreamNode>(result[1]);
}
private void AssertIsProperty(ExpressionNode node, string name)
{
Assert.IsType<PropertyAccessorNode>(node);
@ -145,9 +156,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
private void AssertIsIndexer(ExpressionNode node, params string[] args)
{
Assert.IsType<IndexerNode>(node);
Assert.IsType<StringIndexerNode>(node);
var e = (IndexerNode)node;
var e = (StringIndexerNode)node;
Assert.Equal(e.Arguments.ToArray(), args);
}

23
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionNodeBuilderTests_Errors.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs

@ -2,73 +2,74 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
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<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("1Foo"));
() => ExpressionObserverBuilder.Parse("1Foo"));
}
[Fact]
public void Identifier_Cannot_Start_With_Symbol()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.%Bar"));
() => ExpressionObserverBuilder.Parse("Foo.%Bar"));
}
[Fact]
public void Expression_Cannot_End_With_Period()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar."));
() => ExpressionObserverBuilder.Parse("Foo.Bar."));
}
[Fact]
public void Expression_Cannot_Have_Empty_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[]"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[]"));
}
[Fact]
public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[,3,4]"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[,3,4]"));
}
[Fact]
public void Expression_Cannot_Have_Extra_Comma_In_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => 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<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4,]"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[3,4,]"));
}
[Fact]
public void Expression_Cannot_Have_Digit_After_Indexer()
{
Assert.Throws<ExpressionParseException>(
() => 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<ExpressionParseException>(
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]A"));
() => ExpressionObserverBuilder.Parse("Foo.Bar[3,4]A"));
}
}
}

165
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs

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

59
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs

@ -0,0 +1,59 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Data.Core;
using Xunit;
using Avalonia.Markup.Parsers;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_AvaloniaProperty
{
public ExpressionObserverBuilderTests_AvaloniaProperty()
{
var foo = Class1.FooProperty;
}
[Fact]
public async Task Should_Get_AvaloniaProperty_By_Name()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "Foo");
var result = await target.Take(1);
Assert.Equal("foo", result);
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
[Fact]
public void Should_Track_AvaloniaProperty_By_Name()
{
var data = new Class1();
var target = ExpressionObserverBuilder.Build(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.SetValue(Class1.FooProperty, "bar");
Assert.Equal(new[] { "foo", "bar" }, result);
sub.Dispose();
Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers());
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", defaultValue: "foo");
public string ClrProperty { get; } = "clr-property";
}
}
}

371
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs

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

13
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs → tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs

@ -1,5 +1,6 @@
using Avalonia.Data;
using Avalonia.Data.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<int, int>)result;
@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Return_Error_Notification_If_Too_Many_Parameters(string methodName)
{
var data = new TestObject();
var observer = new ExpressionObserver(data, methodName);
var observer = ExpressionObserverBuilder.Build(data, methodName);
var result = await observer.Take(1);
Assert.IsType<BindingNotification>(result);

112
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs

@ -0,0 +1,112 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Markup.Parsers;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class ExpressionObserverBuilderTests_Negation
{
[Fact]
public async Task Should_Negate_0()
{
var data = new { Foo = 0 };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_1()
{
var data = new { Foo = 1 };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_False_String()
{
var data = new { Foo = "false" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.True((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Negate_True_String()
{
var data = new { Foo = "True" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.False((bool)result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'foo' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'System.Object' to bool."),
BindingErrorType.Error),
result);
GC.KeepAlive(data);
}
[Fact]
public void SetValue_Should_Return_False_For_Invalid_Value()
{
var data = new { Foo = "foo" };
var target = ExpressionObserverBuilder.Build(data, "!Foo");
target.Subscribe(_ => { });
Assert.False(target.SetValue("bar"));
GC.KeepAlive(data);
}
private class Test
{
public bool Foo { get; set; }
}
}
}

42
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs

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

2
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
{

2
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
{

Loading…
Cancel
Save