109 changed files with 3746 additions and 1485 deletions
@ -0,0 +1,60 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reactive.Linq; |
|||
using System.Text; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Data.Core |
|||
{ |
|||
public class AvaloniaPropertyAccessorNode : SettableNode |
|||
{ |
|||
private IDisposable _subscription; |
|||
private readonly bool _enableValidation; |
|||
private readonly AvaloniaProperty _property; |
|||
|
|||
public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation) |
|||
{ |
|||
_property = property; |
|||
_enableValidation = enableValidation; |
|||
} |
|||
|
|||
public override string Description => PropertyName; |
|||
public string PropertyName { get; } |
|||
public override Type PropertyType => _property.PropertyType; |
|||
|
|||
protected override bool SetTargetValueCore(object value, BindingPriority priority) |
|||
{ |
|||
try |
|||
{ |
|||
if (Target.IsAlive && Target.Target is IAvaloniaObject obj) |
|||
{ |
|||
obj.SetValue(_property, value, priority); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
catch |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
protected override void StartListeningCore(WeakReference reference) |
|||
{ |
|||
if (reference.Target is IAvaloniaObject obj) |
|||
{ |
|||
_subscription = new AvaloniaPropertyObservable<object>(obj, _property).Subscribe(ValueChanged); |
|||
} |
|||
else |
|||
{ |
|||
_subscription = null; |
|||
} |
|||
} |
|||
|
|||
protected override void StopListeningCore() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
} |
|||
} |
|||
} |
|||
@ -1,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; |
|||
} |
|||
} |
|||
} |
|||
@ -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?; |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Reactive.Linq; |
|||
using System.Reflection; |
|||
using System.Text; |
|||
using Avalonia.Data; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Data.Core |
|||
{ |
|||
public abstract class IndexerNodeBase : SettableNode |
|||
{ |
|||
private IDisposable _subscription; |
|||
|
|||
protected override void StartListeningCore(WeakReference reference) |
|||
{ |
|||
var target = reference.Target; |
|||
var incc = target as INotifyCollectionChanged; |
|||
var inpc = target as INotifyPropertyChanged; |
|||
var inputs = new List<IObservable<object>>(); |
|||
|
|||
if (incc != null) |
|||
{ |
|||
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>( |
|||
incc, |
|||
nameof(incc.CollectionChanged)) |
|||
.Where(x => ShouldUpdate(x.Sender, x.EventArgs)) |
|||
.Select(_ => GetValue(target))); |
|||
} |
|||
|
|||
if (inpc != null) |
|||
{ |
|||
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>( |
|||
inpc, |
|||
nameof(inpc.PropertyChanged)) |
|||
.Where(x => ShouldUpdate(x.Sender, x.EventArgs)) |
|||
.Select(_ => GetValue(target))); |
|||
} |
|||
|
|||
_subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged); |
|||
} |
|||
|
|||
protected override void StopListeningCore() |
|||
{ |
|||
_subscription.Dispose(); |
|||
} |
|||
|
|||
protected abstract object GetValue(object target); |
|||
|
|||
protected abstract int? TryGetFirstArgumentAsInt(); |
|||
|
|||
private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (sender is IList) |
|||
{ |
|||
var index = TryGetFirstArgumentAsInt(); |
|||
|
|||
if (index == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
return index >= e.NewStartingIndex; |
|||
case NotifyCollectionChangedAction.Remove: |
|||
return index >= e.OldStartingIndex; |
|||
case NotifyCollectionChangedAction.Replace: |
|||
return index >= e.NewStartingIndex && |
|||
index < e.NewStartingIndex + e.NewItems.Count; |
|||
case NotifyCollectionChangedAction.Move: |
|||
return (index >= e.NewStartingIndex && |
|||
index < e.NewStartingIndex + e.NewItems.Count) || |
|||
(index >= e.OldStartingIndex && |
|||
index < e.OldStartingIndex + e.OldItems.Count); |
|||
case NotifyCollectionChangedAction.Reset: |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return true; // Implementation defined meaning for the index, so just try to update anyway
|
|||
} |
|||
|
|||
protected abstract bool ShouldUpdate(object sender, PropertyChangedEventArgs e); |
|||
} |
|||
} |
|||
@ -1,199 +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 System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Data.Core.Parsers |
|||
{ |
|||
internal class ExpressionParser |
|||
{ |
|||
private bool _enableValidation; |
|||
|
|||
public ExpressionParser(bool enableValidation) |
|||
{ |
|||
_enableValidation = enableValidation; |
|||
} |
|||
|
|||
public ExpressionNode Parse(Reader r) |
|||
{ |
|||
var nodes = new List<ExpressionNode>(); |
|||
var state = State.Start; |
|||
|
|||
while (!r.End && state != State.End) |
|||
{ |
|||
switch (state) |
|||
{ |
|||
case State.Start: |
|||
state = ParseStart(r, nodes); |
|||
break; |
|||
|
|||
case State.AfterMember: |
|||
state = ParseAfterMember(r, nodes); |
|||
break; |
|||
|
|||
case State.BeforeMember: |
|||
state = ParseBeforeMember(r, nodes); |
|||
break; |
|||
|
|||
case State.AttachedProperty: |
|||
state = ParseAttachedProperty(r, nodes); |
|||
break; |
|||
|
|||
case State.Indexer: |
|||
state = ParseIndexer(r, nodes); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (state == State.BeforeMember) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Unexpected end of expression."); |
|||
} |
|||
|
|||
for (int n = 0; n < nodes.Count - 1; ++n) |
|||
{ |
|||
nodes[n].Next = nodes[n + 1]; |
|||
} |
|||
|
|||
return nodes.FirstOrDefault(); |
|||
} |
|||
|
|||
private State ParseStart(Reader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseNot(r)) |
|||
{ |
|||
nodes.Add(new LogicalNotNode()); |
|||
return State.Start; |
|||
} |
|||
else if (ParseOpenBrace(r)) |
|||
{ |
|||
return State.AttachedProperty; |
|||
} |
|||
else if (PeekOpenBracket(r)) |
|||
{ |
|||
return State.Indexer; |
|||
} |
|||
else |
|||
{ |
|||
var identifier = IdentifierParser.Parse(r); |
|||
|
|||
if (identifier != null) |
|||
{ |
|||
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); |
|||
return State.AfterMember; |
|||
} |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private static State ParseAfterMember(Reader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseMemberAccessor(r)) |
|||
{ |
|||
return State.BeforeMember; |
|||
} |
|||
else if (ParseStreamOperator(r)) |
|||
{ |
|||
nodes.Add(new StreamNode()); |
|||
return State.AfterMember; |
|||
} |
|||
else if (PeekOpenBracket(r)) |
|||
{ |
|||
return State.Indexer; |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseOpenBrace(r)) |
|||
{ |
|||
return State.AttachedProperty; |
|||
} |
|||
else |
|||
{ |
|||
var identifier = IdentifierParser.Parse(r); |
|||
|
|||
if (identifier != null) |
|||
{ |
|||
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); |
|||
return State.AfterMember; |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
} |
|||
|
|||
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes) |
|||
{ |
|||
var owner = IdentifierParser.Parse(r); |
|||
|
|||
if (r.End || !r.TakeIf('.')) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Invalid attached property name."); |
|||
} |
|||
|
|||
var name = IdentifierParser.Parse(r); |
|||
|
|||
if (r.End || !r.TakeIf(')')) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Expected ')'."); |
|||
} |
|||
|
|||
nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation)); |
|||
return State.AfterMember; |
|||
} |
|||
|
|||
private State ParseIndexer(Reader r, List<ExpressionNode> nodes) |
|||
{ |
|||
var args = ArgumentListParser.Parse(r, '[', ']'); |
|||
|
|||
if (args.Count == 0) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Indexer may not be empty."); |
|||
} |
|||
|
|||
nodes.Add(new IndexerNode(args)); |
|||
return State.AfterMember; |
|||
} |
|||
|
|||
private static bool ParseNot(Reader r) |
|||
{ |
|||
return !r.End && r.TakeIf('!'); |
|||
} |
|||
|
|||
private static bool ParseMemberAccessor(Reader r) |
|||
{ |
|||
return !r.End && r.TakeIf('.'); |
|||
} |
|||
|
|||
private static bool ParseOpenBrace(Reader r) |
|||
{ |
|||
return !r.End && r.TakeIf('('); |
|||
} |
|||
|
|||
private static bool PeekOpenBracket(Reader r) |
|||
{ |
|||
return !r.End && r.Peek == '['; |
|||
} |
|||
|
|||
private static bool ParseStreamOperator(Reader r) |
|||
{ |
|||
return !r.End && r.TakeIf('^'); |
|||
} |
|||
|
|||
private enum State |
|||
{ |
|||
Start, |
|||
AfterMember, |
|||
BeforeMember, |
|||
AttachedProperty, |
|||
Indexer, |
|||
End, |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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}."); |
|||
} |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// FIFO Queue optimized for holding zero or one items.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of items held in the queue.</typeparam>
|
|||
public class SingleOrQueue<T> |
|||
{ |
|||
private T _head; |
|||
private Queue<T> _tail; |
|||
|
|||
private Queue<T> Tail => _tail ?? (_tail = new Queue<T>()); |
|||
|
|||
private bool HasTail => _tail != null; |
|||
|
|||
public bool Empty { get; private set; } = true; |
|||
|
|||
public void Enqueue(T value) |
|||
{ |
|||
if (Empty) |
|||
{ |
|||
_head = value; |
|||
} |
|||
else |
|||
{ |
|||
Tail.Enqueue(value); |
|||
} |
|||
|
|||
Empty = false; |
|||
} |
|||
|
|||
public T Dequeue() |
|||
{ |
|||
if (Empty) |
|||
{ |
|||
throw new InvalidOperationException("Cannot dequeue from an empty queue!"); |
|||
} |
|||
|
|||
var result = _head; |
|||
|
|||
if (HasTail && Tail.Count != 0) |
|||
{ |
|||
_head = Tail.Dequeue(); |
|||
} |
|||
else |
|||
{ |
|||
_head = default; |
|||
Empty = true; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using System; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.Markup.Parsers; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Markup.Xaml.Parsers |
|||
{ |
|||
internal class PropertyParser |
|||
{ |
|||
public (string ns, string owner, string name) Parse(CharacterReader r) |
|||
{ |
|||
if (r.End) |
|||
{ |
|||
throw new ExpressionParseException(0, "Expected property name."); |
|||
} |
|||
|
|||
var openParens = r.TakeIf('('); |
|||
bool closeParens = false; |
|||
string ns = null; |
|||
string owner = null; |
|||
string name = null; |
|||
|
|||
do |
|||
{ |
|||
var token = IdentifierParser.Parse(r); |
|||
|
|||
if (token == null) |
|||
{ |
|||
if (r.End) |
|||
{ |
|||
break; |
|||
} |
|||
else |
|||
{ |
|||
if (openParens && !r.End && (closeParens = r.TakeIf(')'))) |
|||
{ |
|||
break; |
|||
} |
|||
else if (openParens) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, $"Expected ')'."); |
|||
} |
|||
|
|||
throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'."); |
|||
} |
|||
} |
|||
else if (!r.End && r.TakeIf(':')) |
|||
{ |
|||
ns = ns == null ? |
|||
token : |
|||
throw new ExpressionParseException(r.Position, "Unexpected ':'."); |
|||
} |
|||
else if (!r.End && r.TakeIf('.')) |
|||
{ |
|||
owner = owner == null ? |
|||
token : |
|||
throw new ExpressionParseException(r.Position, "Unexpected '.'."); |
|||
} |
|||
else |
|||
{ |
|||
name = token; |
|||
} |
|||
} while (!r.End); |
|||
|
|||
if (name == null) |
|||
{ |
|||
throw new ExpressionParseException(0, "Expected property name."); |
|||
} |
|||
else if (openParens && owner == null) |
|||
{ |
|||
throw new ExpressionParseException(1, "Expected property owner."); |
|||
} |
|||
else if (openParens && !closeParens) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Expected ')'."); |
|||
} |
|||
else if (!r.End) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Expected end of expression."); |
|||
} |
|||
|
|||
return (ns, owner, name); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.Utilities; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reactive; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Markup.Parsers |
|||
{ |
|||
public static class ExpressionObserverBuilder |
|||
{ |
|||
internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(expression)) |
|||
{ |
|||
return (new EmptyExpressionNode(), default); |
|||
} |
|||
|
|||
var reader = new CharacterReader(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).Node, |
|||
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).Node, |
|||
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).Node, |
|||
update, |
|||
description ?? expression); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,330 @@ |
|||
// 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 Avalonia.Utilities; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Markup.Parsers |
|||
{ |
|||
internal enum SourceMode |
|||
{ |
|||
Data, |
|||
Control |
|||
} |
|||
|
|||
internal class ExpressionParser |
|||
{ |
|||
private readonly bool _enableValidation; |
|||
private readonly Func<string, string, Type> _typeResolver; |
|||
|
|||
public ExpressionParser(bool enableValidation, Func<string, string, Type> typeResolver) |
|||
{ |
|||
_typeResolver = typeResolver; |
|||
_enableValidation = enableValidation; |
|||
} |
|||
|
|||
public (ExpressionNode Node, SourceMode Mode) Parse(CharacterReader r) |
|||
{ |
|||
var nodes = new List<ExpressionNode>(); |
|||
var state = State.Start; |
|||
var mode = SourceMode.Data; |
|||
|
|||
while (!r.End && state != State.End) |
|||
{ |
|||
switch (state) |
|||
{ |
|||
case State.Start: |
|||
state = ParseStart(r, nodes); |
|||
break; |
|||
|
|||
case State.AfterMember: |
|||
state = ParseAfterMember(r, nodes); |
|||
break; |
|||
|
|||
case State.BeforeMember: |
|||
state = ParseBeforeMember(r, nodes); |
|||
break; |
|||
|
|||
case State.AttachedProperty: |
|||
state = ParseAttachedProperty(r, nodes); |
|||
break; |
|||
|
|||
case State.Indexer: |
|||
state = ParseIndexer(r, nodes); |
|||
break; |
|||
|
|||
case State.ElementName: |
|||
state = ParseElementName(r, nodes); |
|||
mode = SourceMode.Control; |
|||
break; |
|||
|
|||
case State.RelativeSource: |
|||
state = ParseRelativeSource(r, nodes); |
|||
mode = SourceMode.Control; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (state == State.BeforeMember) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Unexpected end of expression."); |
|||
} |
|||
|
|||
for (int n = 0; n < nodes.Count - 1; ++n) |
|||
{ |
|||
nodes[n].Next = nodes[n + 1]; |
|||
} |
|||
|
|||
return (nodes.FirstOrDefault(), mode); |
|||
} |
|||
|
|||
private State ParseStart(CharacterReader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseNot(r)) |
|||
{ |
|||
nodes.Add(new LogicalNotNode()); |
|||
return State.Start; |
|||
} |
|||
else if (ParseSharp(r)) |
|||
{ |
|||
return State.ElementName; |
|||
} |
|||
else if (ParseDollarSign(r)) |
|||
{ |
|||
return State.RelativeSource; |
|||
} |
|||
else if (ParseOpenBrace(r)) |
|||
{ |
|||
return State.AttachedProperty; |
|||
} |
|||
else if (PeekOpenBracket(r)) |
|||
{ |
|||
return State.Indexer; |
|||
} |
|||
else |
|||
{ |
|||
var identifier = IdentifierParser.Parse(r); |
|||
|
|||
if (identifier != null) |
|||
{ |
|||
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); |
|||
return State.AfterMember; |
|||
} |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private static State ParseAfterMember(CharacterReader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseMemberAccessor(r)) |
|||
{ |
|||
return State.BeforeMember; |
|||
} |
|||
else if (ParseStreamOperator(r)) |
|||
{ |
|||
nodes.Add(new StreamNode()); |
|||
return State.AfterMember; |
|||
} |
|||
else if (PeekOpenBracket(r)) |
|||
{ |
|||
return State.Indexer; |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private State ParseBeforeMember(CharacterReader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseOpenBrace(r)) |
|||
{ |
|||
return State.AttachedProperty; |
|||
} |
|||
else |
|||
{ |
|||
var identifier = IdentifierParser.Parse(r); |
|||
|
|||
if (identifier != null) |
|||
{ |
|||
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation)); |
|||
return State.AfterMember; |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
} |
|||
|
|||
private State ParseAttachedProperty(CharacterReader r, List<ExpressionNode> nodes) |
|||
{ |
|||
var (ns, owner) = ParseTypeName(r); |
|||
|
|||
if (r.End || !r.TakeIf('.')) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Invalid attached property name."); |
|||
} |
|||
|
|||
var name = IdentifierParser.Parse(r); |
|||
|
|||
if (r.End || !r.TakeIf(')')) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Expected ')'."); |
|||
} |
|||
|
|||
if (_typeResolver == null) |
|||
{ |
|||
throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); |
|||
} |
|||
|
|||
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns, owner), name); |
|||
|
|||
nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation)); |
|||
return State.AfterMember; |
|||
} |
|||
|
|||
private State ParseIndexer(CharacterReader r, List<ExpressionNode> nodes) |
|||
{ |
|||
var args = ArgumentListParser.Parse(r, '[', ']'); |
|||
|
|||
if (args.Count == 0) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Indexer may not be empty."); |
|||
} |
|||
|
|||
nodes.Add(new StringIndexerNode(args)); |
|||
return State.AfterMember; |
|||
} |
|||
|
|||
private State ParseElementName(CharacterReader r, List<ExpressionNode> nodes) |
|||
{ |
|||
var name = IdentifierParser.Parse(r); |
|||
|
|||
if (name == null) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Element name expected after '#'."); |
|||
} |
|||
|
|||
nodes.Add(new ElementNameNode(name)); |
|||
return State.AfterMember; |
|||
} |
|||
|
|||
|
|||
private State ParseRelativeSource(CharacterReader r, List<ExpressionNode> nodes) |
|||
{ |
|||
var mode = IdentifierParser.Parse(r); |
|||
|
|||
if (mode == "self") |
|||
{ |
|||
nodes.Add(new SelfNode()); |
|||
} |
|||
else if (mode == "parent") |
|||
{ |
|||
Type ancestorType = null; |
|||
var ancestorLevel = 0; |
|||
if (PeekOpenBracket(r)) |
|||
{ |
|||
var args = ArgumentListParser.Parse(r, '[', ']', ';'); |
|||
if (args.Count > 2 || args.Count == 0) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); |
|||
} |
|||
else if (args.Count == 1) |
|||
{ |
|||
if (int.TryParse(args[0], out int level)) |
|||
{ |
|||
ancestorType = null; |
|||
ancestorLevel = level; |
|||
} |
|||
else |
|||
{ |
|||
var typeName = ParseTypeName(new CharacterReader(args[0])); |
|||
ancestorType = _typeResolver(typeName.ns, typeName.typeName); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
var typeName = ParseTypeName(new CharacterReader(args[0])); |
|||
ancestorType = _typeResolver(typeName.ns, typeName.typeName); |
|||
ancestorLevel = int.Parse(args[1]); |
|||
} |
|||
} |
|||
nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel)); |
|||
} |
|||
else |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); |
|||
} |
|||
|
|||
return State.AfterMember; |
|||
} |
|||
|
|||
private static (string ns, string typeName) ParseTypeName(CharacterReader r) |
|||
{ |
|||
string ns, typeName; |
|||
ns = string.Empty; |
|||
var typeNameOrNamespace = IdentifierParser.Parse(r); |
|||
|
|||
if (!r.End && r.TakeIf(':')) |
|||
{ |
|||
ns = typeNameOrNamespace; |
|||
typeName = IdentifierParser.Parse(r); |
|||
} |
|||
else |
|||
{ |
|||
typeName = typeNameOrNamespace; |
|||
} |
|||
|
|||
return (ns, typeName); |
|||
} |
|||
|
|||
private static bool ParseNot(CharacterReader r) |
|||
{ |
|||
return !r.End && r.TakeIf('!'); |
|||
} |
|||
|
|||
private static bool ParseMemberAccessor(CharacterReader r) |
|||
{ |
|||
return !r.End && r.TakeIf('.'); |
|||
} |
|||
|
|||
private static bool ParseOpenBrace(CharacterReader r) |
|||
{ |
|||
return !r.End && r.TakeIf('('); |
|||
} |
|||
|
|||
private static bool PeekOpenBracket(CharacterReader r) |
|||
{ |
|||
return !r.End && r.Peek == '['; |
|||
} |
|||
|
|||
private static bool ParseStreamOperator(CharacterReader r) |
|||
{ |
|||
return !r.End && r.TakeIf('^'); |
|||
} |
|||
|
|||
private static bool ParseDollarSign(CharacterReader r) |
|||
{ |
|||
return !r.End && r.TakeIf('$'); |
|||
} |
|||
|
|||
private static bool ParseSharp(CharacterReader r) |
|||
{ |
|||
return !r.End && r.TakeIf('#'); |
|||
} |
|||
|
|||
private enum State |
|||
{ |
|||
Start, |
|||
RelativeSource, |
|||
ElementName, |
|||
AfterMember, |
|||
BeforeMember, |
|||
AttachedProperty, |
|||
Indexer, |
|||
End, |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.LogicalTree; |
|||
|
|||
namespace Avalonia.Markup.Parsers.Nodes |
|||
{ |
|||
internal class ElementNameNode : ExpressionNode |
|||
{ |
|||
private readonly string _name; |
|||
private IDisposable _subscription; |
|||
|
|||
public ElementNameNode(string name) |
|||
{ |
|||
_name = name; |
|||
} |
|||
|
|||
public override string Description => $"#{_name}"; |
|||
|
|||
protected override void StartListeningCore(WeakReference reference) |
|||
{ |
|||
if (reference.Target is ILogical logical) |
|||
{ |
|||
_subscription = ControlLocator.Track(logical, _name).Subscribe(ValueChanged); |
|||
} |
|||
else |
|||
{ |
|||
_subscription = null; |
|||
} |
|||
} |
|||
|
|||
protected override void StopListeningCore() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.LogicalTree; |
|||
|
|||
namespace Avalonia.Markup.Parsers.Nodes |
|||
{ |
|||
internal class FindAncestorNode : ExpressionNode |
|||
{ |
|||
private readonly int _level; |
|||
private readonly Type _ancestorType; |
|||
private IDisposable _subscription; |
|||
|
|||
public FindAncestorNode(Type ancestorType, int level) |
|||
{ |
|||
_level = level; |
|||
_ancestorType = ancestorType; |
|||
} |
|||
|
|||
public override string Description |
|||
{ |
|||
get |
|||
{ |
|||
if (_ancestorType == null) |
|||
{ |
|||
return $"$parent[{_level}]"; |
|||
} |
|||
else |
|||
{ |
|||
return $"$parent[{_ancestorType.Name}, {_level}]"; |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override void StartListeningCore(WeakReference reference) |
|||
{ |
|||
if (reference.Target is ILogical logical) |
|||
{ |
|||
_subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged); |
|||
} |
|||
else |
|||
{ |
|||
_subscription = null; |
|||
} |
|||
} |
|||
|
|||
protected override void StopListeningCore() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Data.Core; |
|||
|
|||
namespace Avalonia.Markup.Parsers.Nodes |
|||
{ |
|||
internal class SelfNode : ExpressionNode |
|||
{ |
|||
public override string Description => "$self"; |
|||
} |
|||
} |
|||
@ -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() { } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using Avalonia.Utilities; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Utilities |
|||
{ |
|||
public class SingleOrQueueTests |
|||
{ |
|||
[Fact] |
|||
public void New_SingleOrQueue_Is_Empty() |
|||
{ |
|||
Assert.True(new SingleOrQueue<object>().Empty); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Dequeue_Throws_When_Empty() |
|||
{ |
|||
var queue = new SingleOrQueue<object>(); |
|||
|
|||
Assert.Throws<InvalidOperationException>(() => queue.Dequeue()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Enqueue_Adds_Element() |
|||
{ |
|||
var queue = new SingleOrQueue<int>(); |
|||
|
|||
queue.Enqueue(1); |
|||
|
|||
Assert.False(queue.Empty); |
|||
|
|||
Assert.Equal(1, queue.Dequeue()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Multiple_Elements_Dequeued_In_Correct_Order() |
|||
{ |
|||
var queue = new SingleOrQueue<int>(); |
|||
|
|||
queue.Enqueue(1); |
|||
queue.Enqueue(2); |
|||
queue.Enqueue(3); |
|||
Assert.Equal(1, queue.Dequeue()); |
|||
Assert.Equal(2, queue.Dequeue()); |
|||
Assert.Equal(3, queue.Dequeue()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Data; |
|||
using Avalonia.Data.Converters; |
|||
using Avalonia.VisualTree; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Markup.UnitTests.Data |
|||
{ |
|||
public class TemplateBindingTests |
|||
{ |
|||
[Fact] |
|||
public void OneWay_Binding_Should_Be_Set_Up() |
|||
{ |
|||
var source = new Button |
|||
{ |
|||
Template = new FuncControlTemplate<Button>(parent => |
|||
new ContentPresenter |
|||
{ |
|||
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty) |
|||
}), |
|||
}; |
|||
|
|||
source.ApplyTemplate(); |
|||
|
|||
var target = (ContentPresenter)source.GetVisualChildren().Single(); |
|||
|
|||
Assert.Null(target.Content); |
|||
source.Content = "foo"; |
|||
Assert.Equal("foo", target.Content); |
|||
source.Content = "bar"; |
|||
Assert.Equal("bar", target.Content); |
|||
} |
|||
|
|||
[Fact] |
|||
public void TwoWay_Binding_Should_Be_Set_Up() |
|||
{ |
|||
var source = new Button |
|||
{ |
|||
Template = new FuncControlTemplate<Button>(parent => |
|||
new ContentPresenter |
|||
{ |
|||
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty) |
|||
{ |
|||
Mode = BindingMode.TwoWay, |
|||
} |
|||
}), |
|||
}; |
|||
|
|||
source.ApplyTemplate(); |
|||
|
|||
var target = (ContentPresenter)source.GetVisualChildren().Single(); |
|||
|
|||
Assert.Null(target.Content); |
|||
source.Content = "foo"; |
|||
Assert.Equal("foo", target.Content); |
|||
target.Content = "bar"; |
|||
Assert.Equal("bar", source.Content); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Converter_Should_Be_Used() |
|||
{ |
|||
var source = new Button |
|||
{ |
|||
Template = new FuncControlTemplate<Button>(parent => |
|||
new ContentPresenter |
|||
{ |
|||
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty) |
|||
{ |
|||
Mode = BindingMode.TwoWay, |
|||
Converter = new PrefixConverter(), |
|||
ConverterParameter = "Hello ", |
|||
} |
|||
}), |
|||
}; |
|||
|
|||
source.ApplyTemplate(); |
|||
|
|||
var target = (ContentPresenter)source.GetVisualChildren().Single(); |
|||
|
|||
Assert.Null(target.Content); |
|||
source.Content = "foo"; |
|||
Assert.Equal("Hello foo", target.Content); |
|||
target.Content = "Hello bar"; |
|||
Assert.Equal("bar", source.Content); |
|||
} |
|||
|
|||
private class PrefixConverter : IValueConverter |
|||
{ |
|||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) |
|||
{ |
|||
if (value != null && parameter != null) |
|||
{ |
|||
return parameter.ToString() + value; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) |
|||
{ |
|||
if (value != null && parameter != null) |
|||
{ |
|||
var s = value.ToString(); |
|||
var prefix = parameter.ToString(); |
|||
|
|||
if (s.StartsWith(prefix) == true) |
|||
{ |
|||
return s.Substring(prefix.Length); |
|||
} |
|||
|
|||
return s; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); } |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue