Browse Source

Preliminary support. Indexers require more work since the compiler doesn't generate IndexExpressions (they weren't in S.L.Expressions v1 so they aren't auto-genned).

pull/1667/head
Jeremy Koritzinsky 8 years ago
parent
commit
e8c32bf801
  1. 8
      src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs
  2. 92
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  3. 4
      src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs
  4. 61
      src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs
  5. 80
      src/Markup/Avalonia.Markup/Data/IndexerNode.cs
  6. 87
      src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs
  7. 34
      src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs
  8. 158
      src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs
  9. 111
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs

8
src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq.Expressions;
using Avalonia.Markup.Data.Parsers;
namespace Avalonia.Markup.Data
@ -26,5 +27,12 @@ namespace Avalonia.Markup.Data
return node;
}
public static ExpressionNode Build(LambdaExpression expression, bool enableValidation = false)
{
var parser = new ExpressionTreeParser(enableValidation);
return parser.Parse(expression);
}
}
}

92
src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@ -72,20 +73,36 @@ namespace Avalonia.Markup.Data
string expression,
bool enableDataValidation = false,
string description = null)
: this(root, Parse(expression, enableDataValidation), description ?? expression)
{
Contract.Requires<ArgumentNullException>(expression != null);
Expression = expression;
}
private ExpressionObserver(
object root,
ExpressionNode node,
string description = null)
{
if (root == AvaloniaProperty.UnsetValue)
{
root = null;
}
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_node = node;
Description = description;
_root = new WeakReference(root);
}
public static ExpressionObserver CreateFromExpression<T, 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>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
@ -100,15 +117,38 @@ namespace Avalonia.Markup.Data
string expression,
bool enableDataValidation = false,
string description = null)
: this(rootObservable, Parse(expression, enableDataValidation), description ?? expression)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
}
private ExpressionObserver(
IObservable<object> rootObservable,
ExpressionNode node,
string description)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
_node = node;
Description = description;
_root = rootObservable;
_finished = new Subject<Unit>();
}
public static ExpressionObserver CreateFromExpression<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>
@ -127,19 +167,44 @@ namespace Avalonia.Markup.Data
IObservable<Unit> update,
bool enableDataValidation = false,
string description = null)
: this(rootGetter, Parse(expression, enableDataValidation), update, description ?? expression)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
Contract.Requires<ArgumentNullException>(update != null);
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
}
private ExpressionObserver(
Func<object> rootGetter,
ExpressionNode node,
IObservable<Unit> update,
string description)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(update != null);
Description = description;
_node = node;
_finished = new Subject<Unit>();
_node.Target = new WeakReference(rootGetter());
_root = update.Select(x => rootGetter());
}
public static ExpressionObserver CreateFromExpression<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.
@ -238,6 +303,11 @@ namespace Avalonia.Markup.Data
}
}
private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation)
{
return ExpressionNodeBuilder.Build(expression, enableDataValidation);
}
private static object ToWeakReference(object o)
{
return o is BindingNotification ? o : new WeakReference(o);

4
src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs

@ -17,8 +17,8 @@ namespace Avalonia.Markup.Data
/// </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;
}

61
src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Text;
using Avalonia.Data;
namespace Avalonia.Markup.Data
{
class IndexerExpressionNode : IndexerNodeBase
{
private readonly ParameterExpression parameter;
private readonly IndexExpression expression;
private readonly Delegate setDelegate;
private readonly Delegate getDelegate;
private readonly Delegate firstArgumentDelegate;
public IndexerExpressionNode(IndexExpression expression)
{
parameter = Expression.Parameter(expression.Object.Type);
this.expression = expression.Update(parameter, expression.Arguments);
getDelegate = Expression.Lambda(this.expression, parameter).Compile();
var valueParameter = Expression.Parameter(expression.Type);
setDelegate = Expression.Lambda(Expression.Assign(this.expression, valueParameter), parameter, valueParameter).Compile();
firstArgumentDelegate = Expression.Lambda(this.expression.Arguments[0], parameter).Compile();
}
public override Type PropertyType => expression.Type;
public override string Description => expression.ToString();
public override bool SetTargetValue(object value, BindingPriority priority)
{
try
{
setDelegate.DynamicInvoke(Target.Target, value);
return true;
}
catch (Exception)
{
return false;
}
}
protected override object GetValue(object target)
{
return getDelegate.DynamicInvoke(target);
}
protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
{
return expression.Indexer.Name == e.PropertyName;
}
protected override int? TryGetFirstArgumentAsInt() => firstArgumentDelegate.DynamicInvoke(Target.Target) as int?;
}
}

80
src/Markup/Avalonia.Markup/Data/IndexerNode.cs

@ -15,7 +15,7 @@ using Avalonia.Data;
namespace Avalonia.Markup.Data
{
internal class IndexerNode : ExpressionNode, ISettableNode
internal class IndexerNode : IndexerNodeBase
{
public IndexerNode(IList<string> arguments)
{
@ -24,35 +24,7 @@ namespace Avalonia.Markup.Data
public override string Description => "[" + string.Join(",", Arguments) + "]";
protected override IObservable<object> 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)));
}
return Observable.Merge(inputs).StartWith(GetValue(target));
}
public bool SetTargetValue(object value, BindingPriority priority)
public override bool SetTargetValue(object value, BindingPriority priority)
{
var typeInfo = Target.Target.GetType().GetTypeInfo();
var list = Target.Target as IList;
@ -154,9 +126,9 @@ namespace Avalonia.Markup.Data
public IList<string> Arguments { get; }
public Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType;
public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType;
private object GetValue(object target)
protected override object GetValue(object target)
{
var typeInfo = target.GetType().GetTypeInfo();
var list = target as IList;
@ -309,45 +281,19 @@ namespace Avalonia.Markup.Data
}
}
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;
}
}
}

87
src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs

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

34
src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs

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

158
src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
namespace Avalonia.Markup.Data.Parsers
{
class ExpressionVisitorNodeBuilder : ExpressionVisitor
{
private static PropertyInfo AvaloniaObjectIndexer;
private readonly bool enableDataValidation;
static ExpressionVisitorNodeBuilder()
{
AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) });
}
public List<ExpressionNode> Nodes { get; }
public ExpressionVisitorNodeBuilder(bool enableDataValidation)
{
this.enableDataValidation = enableDataValidation;
Nodes = new List<ExpressionNode>();
}
protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType != ExpressionType.Not || node.Type != typeof(bool))
{
throw new ExpressionParseException(0, $"Invalid unary operation {node.NodeType} in binding expression");
}
Nodes.Add(new LogicalNotNode());
return base.VisitUnary(node);
}
protected override Expression VisitMember(MemberExpression node)
{
Nodes.Add(new PropertyAccessorNode(node.Member.Name, enableDataValidation));
return base.VisitMember(node);
}
protected override Expression VisitIndex(IndexExpression node)
{
if (node.Indexer == AvaloniaObjectIndexer)
{
var property = GetArgumentExpressionValue<AvaloniaProperty>(node.Arguments[0]);
Nodes.Add(new PropertyAccessorNode($"{property.OwnerType.Name}.{property.Name}", 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 base.VisitBinary(node);
}
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitBlock(BlockExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override CatchBlock VisitCatchBlock(CatchBlock node)
{
throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions.");
}
protected override Expression VisitConditional(ConditionalExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitDynamic(DynamicExpression node)
{
throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions.");
}
protected override ElementInit VisitElementInit(ElementInit node)
{
throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression.");
}
protected override Expression VisitGoto(GotoExpression node)
{
throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions.");
}
protected override Expression VisitInvocation(InvocationExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLabel(LabelExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitListInit(ListInitExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLoop(LoopExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{
throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions.");
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitSwitch(SwitchExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTry(TryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTypeBinary(TypeBinaryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
}
}

111
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class ExpressionObserverTests_ExpressionTree
{
[Fact]
public async Task IdentityExpression_Creates_IdentityObserver()
{
var target = new object();
var observer = ExpressionObserver.CreateFromExpression(target, o => o);
Assert.Equal(target, await observer.Take(1));
}
[Fact]
public async Task Property_Access_Expression_Observes_Property()
{
var target = new Class1();
var observer = ExpressionObserver.CreateFromExpression(target, o => o.Foo);
Assert.Null(await observer.Take(1));
using (observer.Subscribe(_ => {}))
{
target.Foo = "Test";
}
Assert.Equal("Test", await observer.Take(1));
GC.KeepAlive(target);
}
[Fact]
public void Property_Acccess_Expression_Can_Set_Property()
{
var data = new Class1();
var target = ExpressionObserver.CreateFromExpression(data, o => o.Foo);
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue("baz"));
}
GC.KeepAlive(data);
}
[Fact]
public async Task Indexer_Accessor_Can_Read_Value()
{
var data = new[] { 1, 2, 3, 4 };
var target = ExpressionObserver.CreateFromExpression(data, o => o[0]);
Assert.Equal(data[0], await target.Take(1));
}
[Fact]
public async Task Indexer_Accessor_Can_Read_Complex_Index()
{
var data = new Dictionary<object, object>();
var key = new object();
data.Add(key, new object());
var target = ExpressionObserver.CreateFromExpression(data, o => o[key]);
Assert.Equal(data[key], await target.Take(1));
}
[Fact]
public void Indexer_Can_Set_Value()
{
var data = new[] { 1, 2, 3, 4 };
var target = ExpressionObserver.CreateFromExpression(data, o => o[0]);
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue(2));
}
GC.KeepAlive(data);
}
private class Class1 : NotifyingBase
{
private string _foo;
public string Foo
{
get { return _foo; }
set
{
_foo = value;
RaisePropertyChanged(nameof(Foo));
}
}
}
}
}
Loading…
Cancel
Save