csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
257 lines
9.3 KiB
257 lines
9.3 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using Avalonia.Data.Core;
|
|
|
|
#nullable enable
|
|
|
|
namespace Avalonia.Base.UnitTests.Data.Core;
|
|
|
|
internal class BindingPathFromExpressionBuilder : ExpressionVisitor
|
|
{
|
|
private static readonly PropertyInfo AvaloniaObjectIndexer;
|
|
private static readonly string IndexerGetterName = "get_Item";
|
|
private const string MultiDimensionalArrayGetterMethodName = "Get";
|
|
private readonly LambdaExpression _rootExpression;
|
|
private readonly StringBuilder _path = new();
|
|
private Expression? _head;
|
|
private int _negationCount;
|
|
private TypeResolver? _resolver;
|
|
|
|
public BindingPathFromExpressionBuilder(LambdaExpression expression)
|
|
{
|
|
_rootExpression = expression;
|
|
}
|
|
|
|
static BindingPathFromExpressionBuilder()
|
|
{
|
|
AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) })!;
|
|
}
|
|
|
|
public static (string, Func<string?, string, Type>?) Build<TIn, TOut>(Expression<Func<TIn, TOut>> expression)
|
|
{
|
|
var visitor = new BindingPathFromExpressionBuilder(expression);
|
|
visitor.Visit(expression);
|
|
visitor._path.Insert(0, new string('!', visitor._negationCount));
|
|
|
|
Func<string?, string, Type>? resolver = null;
|
|
|
|
if (visitor._resolver is not null)
|
|
resolver = visitor._resolver.Resolve;
|
|
|
|
return (visitor._path.ToString(), resolver);
|
|
}
|
|
|
|
protected override Expression VisitBinary(BinaryExpression node)
|
|
{
|
|
// Indexers require more work since the compiler doesn't generate IndexExpressions:
|
|
// they weren't in System.Linq.Expressions v1 and so must be generated manually.
|
|
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 VisitIndex(IndexExpression node)
|
|
{
|
|
if (node.Indexer == AvaloniaObjectIndexer)
|
|
{
|
|
var property = GetValue<AvaloniaProperty>(node.Arguments[0]);
|
|
var name = property.Name;
|
|
|
|
if (property.IsAttached)
|
|
{
|
|
_resolver ??= new();
|
|
_resolver.Add(property.OwnerType.Name, property.OwnerType);
|
|
name = $"({property.OwnerType.Name}.{property.Name})";
|
|
}
|
|
|
|
return Add(node.Object, node, name, ".");
|
|
}
|
|
else
|
|
{
|
|
var indexes = string.Join(',', node.Arguments.Select(GetValue<object>));
|
|
return Add(node.Object, node, $"[{indexes}]");
|
|
}
|
|
}
|
|
|
|
protected override Expression VisitMember(MemberExpression node)
|
|
{
|
|
if (node.Member.MemberType != MemberTypes.Property)
|
|
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
|
|
|
|
var property = (PropertyInfo)node.Member;
|
|
return Add(node.Expression, node, property.Name, ".");
|
|
}
|
|
|
|
protected override Expression VisitMethodCall(MethodCallExpression node)
|
|
{
|
|
var method = node.Method;
|
|
|
|
if (method.Name == IndexerGetterName && node.Object is not null)
|
|
{
|
|
var property = TryGetPropertyFromMethod(method);
|
|
return Visit(Expression.MakeIndex(node.Object, property, node.Arguments));
|
|
}
|
|
else if (method.Name == MultiDimensionalArrayGetterMethodName &&
|
|
node.Object is not null)
|
|
{
|
|
var indexes = string.Join(',', node.Arguments.Select(GetValue<int>));
|
|
return Add(node.Object, node, $"[{indexes}]");
|
|
}
|
|
else if (method.Name.StartsWith(StreamBindingExtensions.StreamBindingName) &&
|
|
method.DeclaringType == typeof(StreamBindingExtensions) &&
|
|
method.GetGenericArguments() is [Type genericArg])
|
|
{
|
|
var instance = node.Method.IsStatic ? node.Arguments[0] : node.Object;
|
|
return Add(instance, node, "^");
|
|
}
|
|
|
|
throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType}.{node.Method.Name}'.");
|
|
}
|
|
|
|
protected override Expression VisitParameter(ParameterExpression node)
|
|
{
|
|
if (node == _rootExpression.Parameters[0] && _head is null)
|
|
_head = node;
|
|
return base.VisitParameter(node);
|
|
}
|
|
|
|
protected override Expression VisitUnary(UnaryExpression node)
|
|
{
|
|
if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool))
|
|
{
|
|
++_negationCount;
|
|
return _head = base.VisitUnary(node);
|
|
}
|
|
else if (node.NodeType == ExpressionType.Convert)
|
|
{
|
|
if (node.Operand.Type.IsAssignableFrom(node.Type))
|
|
{
|
|
// Ignore inheritance casts
|
|
return _head = base.VisitUnary(node);
|
|
}
|
|
}
|
|
else if (node.NodeType == ExpressionType.TypeAs)
|
|
{
|
|
// Ignore as operator.
|
|
return _head = base.VisitUnary(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 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}.");
|
|
}
|
|
|
|
private Expression Add(Expression? instance, Expression expression, string pathSegment, string? separator = null)
|
|
{
|
|
var visited = Visit(instance);
|
|
|
|
if (visited != _head)
|
|
throw new ExpressionParseException(
|
|
0,
|
|
$"Unable to parse '{expression}': expected an instance of '{_head}' but got '{visited}'.");
|
|
|
|
if (_path.Length > 0 && !string.IsNullOrEmpty(separator))
|
|
_path.Append(separator);
|
|
|
|
_path.Append(pathSegment);
|
|
return _head = expression;
|
|
}
|
|
|
|
private static T GetValue<T>(Expression expr)
|
|
{
|
|
if (expr is ConstantExpression constant)
|
|
return (T)constant.Value!;
|
|
return Expression.Lambda<Func<T>>(expr).Compile(preferInterpretation: true)();
|
|
}
|
|
|
|
private static PropertyInfo? TryGetPropertyFromMethod(MethodInfo method)
|
|
{
|
|
var type = method.DeclaringType;
|
|
return type?.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method);
|
|
}
|
|
|
|
private class TypeResolver
|
|
{
|
|
private Dictionary<string, Type> _registered = new();
|
|
public void Add(string name, Type type) => _registered.Add(name, type);
|
|
public Type Resolve(string? ns, string name)
|
|
{
|
|
if (_registered.TryGetValue(name, out var type))
|
|
return type;
|
|
throw new Exception($"Unable to resolve type '{name}'.");
|
|
}
|
|
}
|
|
}
|
|
|