9 changed files with 555 additions and 80 deletions
@ -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?; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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}."); |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue