diff --git a/.ncrunch/Avalonia.HarfBuzz.v3.ncrunchproject b/.ncrunch/Avalonia.HarfBuzz.v3.ncrunchproject
new file mode 100644
index 0000000000..0bcc569d05
--- /dev/null
+++ b/.ncrunch/Avalonia.HarfBuzz.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ False
+
+
\ No newline at end of file
diff --git a/Avalonia.v3.ncrunchsolution b/Avalonia.v3.ncrunchsolution
index 4de91979d6..f675b66f7b 100644
--- a/Avalonia.v3.ncrunchsolution
+++ b/Avalonia.v3.ncrunchsolution
@@ -13,8 +13,9 @@
TargetFrameworks = net10.0
False
+ True
.ncrunch
True
True
-
+
\ No newline at end of file
diff --git a/src/Avalonia.Base/Data/CompiledBinding.cs b/src/Avalonia.Base/Data/CompiledBinding.cs
index 952a5cddc4..e243246b4f 100644
--- a/src/Avalonia.Base/Data/CompiledBinding.cs
+++ b/src/Avalonia.Base/Data/CompiledBinding.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using System.Linq.Expressions;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
@@ -27,6 +29,95 @@ public class CompiledBinding : BindingBase
/// The binding path.
public CompiledBinding(CompiledBindingPath path) => Path = path;
+ ///
+ /// Creates a from a lambda expression.
+ ///
+ /// The input type of the binding expression.
+ /// The output type of the binding expression.
+ ///
+ /// The lambda expression representing the binding path
+ /// (e.g., vm => vm.PropertyName ).
+ ///
+ /// The source object for the binding. If null, uses the target's DataContext.
+ ///
+ ///
+ /// Optional value converter to transform values between source and target.
+ ///
+ ///
+ /// The binding mode. Default is which resolves to the
+ /// property's default binding mode.
+ ///
+ /// The binding priority.
+ /// The culture in which to evaluate the converter.
+ /// A parameter to pass to the converter.
+ ///
+ /// The value to use when the binding is unable to produce a value.
+ ///
+ /// The string format for the binding result.
+ /// The value to use when the binding result is null.
+ ///
+ /// The timing of binding source updates for TwoWay/OneWayToSource bindings.
+ ///
+ ///
+ /// The amount of time, in milliseconds, to wait before updating the binding source.
+ ///
+ ///
+ /// A configured instance ready to be applied to a property.
+ ///
+ ///
+ /// Thrown when the expression contains unsupported operations or invalid syntax for binding
+ /// expressions.
+ ///
+ ///
+ /// This builds a with a path described by a lambda expression.
+ /// The resulting binding avoids reflection for property access, providing better performance
+ /// than reflection-based bindings.
+ ///
+ /// Supported expressions include:
+ ///
+ /// - Property access:
x => x.Property
+ /// - Nested properties:
x => x.Property.Nested
+ /// - Indexers:
x => x.Items[0]
+ /// - Type casts:
x => ((DerivedType)x).Property
+ /// - Logical NOT:
x => !x.BoolProperty
+ /// - Stream bindings:
x => x.TaskProperty (Task/Observable)
+ /// - AvaloniaProperty access:
x => x[MyProperty]
+ ///
+ ///
+ [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
+ [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
+ public static CompiledBinding Create(
+ Expression> expression,
+ object? source = null,
+ IValueConverter? converter = null,
+ BindingMode mode = BindingMode.Default,
+ BindingPriority priority = BindingPriority.LocalValue,
+ CultureInfo? converterCulture = null,
+ object? converterParameter = null,
+ object? fallbackValue = null,
+ string? stringFormat = null,
+ object? targetNullValue = null,
+ UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.Default,
+ int delay = 0)
+ {
+ var path = BindingExpressionVisitor.BuildPath(expression);
+ return new CompiledBinding(path)
+ {
+ Source = source ?? AvaloniaProperty.UnsetValue,
+ Converter = converter,
+ ConverterCulture = converterCulture,
+ ConverterParameter = converterParameter,
+ FallbackValue = fallbackValue ?? AvaloniaProperty.UnsetValue,
+ Mode = mode,
+ Priority = priority,
+ StringFormat = stringFormat,
+ TargetNullValue = targetNullValue ?? AvaloniaProperty.UnsetValue,
+ UpdateSourceTrigger = updateSourceTrigger,
+ Delay = delay
+ };
+ }
+
///
/// Gets or sets the amount of time, in milliseconds, to wait before updating the binding
/// source after the value on the target changes.
diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs
index 6ad1a99f41..00a90f87fe 100644
--- a/src/Avalonia.Base/Data/Core/BindingExpression.cs
+++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs
@@ -173,59 +173,6 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I
_nodes[0].SetSource(source, null);
}
- ///
- /// Creates an from an expression tree.
- ///
- /// The input type of the binding expression.
- /// The output type of the binding expression.
- /// The source from which the binding value will be read.
- /// The expression representing the binding path.
- /// The converter to use.
- /// The converter culture to use.
- /// The converter parameter.
- /// Whether data validation should be enabled for the binding.
- /// The fallback value.
- /// The binding mode.
- /// The binding priority.
- /// The null target value.
- /// Whether to allow reflection for target type conversion.
- [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
-#if NET8_0_OR_GREATER
- [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
-#endif
- internal static BindingExpression Create(
- TIn source,
- Expression> expression,
- IValueConverter? converter = null,
- CultureInfo? converterCulture = null,
- object? converterParameter = null,
- bool enableDataValidation = false,
- Optional fallbackValue = default,
- BindingMode mode = BindingMode.OneWay,
- BindingPriority priority = BindingPriority.LocalValue,
- object? targetNullValue = null,
- bool allowReflection = true)
- where TIn : class?
- {
- var nodes = BindingExpressionVisitor.BuildNodes(expression, enableDataValidation);
- var fallback = fallbackValue.HasValue ? fallbackValue.Value : AvaloniaProperty.UnsetValue;
-
- return new BindingExpression(
- source,
- nodes,
- fallback,
- converter: converter,
- converterCulture: converterCulture,
- converterParameter: converterParameter,
- enableDataValidation: enableDataValidation,
- mode: mode,
- priority: priority,
- targetNullValue: targetNullValue,
- targetTypeConverter: allowReflection ?
- TargetTypeConverter.GetReflectionConverter() :
- TargetTypeConverter.GetDefaultConverter());
- }
-
///
/// Called by an belonging to this binding when its
/// changes.
diff --git a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs
index 1981732145..c354bcc15a 100644
--- a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs
+++ b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs
@@ -1,47 +1,59 @@
using System;
-using System.Collections;
using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
+using System.Threading.Tasks;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.ExpressionNodes.Reflection;
+using Avalonia.Data.Core.Plugins;
+using Avalonia.Reactive;
+using Avalonia.Utilities;
namespace Avalonia.Data.Core.Parsers;
-[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
-#if NET8_0_OR_GREATER
+///
+/// Visits and processes a LINQ expression to build a compiled binding path.
+///
+/// The input parameter type for the binding expression.
+///
+/// This visitor traverses lambda expressions used in compiled bindings and uses
+/// to construct a , which
+/// can then be converted into instances. It supports property access,
+/// indexers, AvaloniaProperty access, stream bindings, type casts, and logical operators.
+///
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
-#endif
-internal class BindingExpressionVisitor : ExpressionVisitor
+[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
+internal class BindingExpressionVisitor(LambdaExpression expression) : ExpressionVisitor
{
- private static readonly PropertyInfo AvaloniaObjectIndexer;
- private static readonly MethodInfo CreateDelegateMethod;
private const string IndexerGetterName = "get_Item";
private const string MultiDimensionalArrayGetterMethodName = "Get";
- private readonly bool _enableDataValidation;
- private readonly LambdaExpression _rootExpression;
- private readonly List _nodes = new();
+ private readonly LambdaExpression _rootExpression = expression;
+ private readonly CompiledBindingPathBuilder _builder = new();
private Expression? _head;
- public BindingExpressionVisitor(LambdaExpression expression, bool enableDataValidation)
- {
- _rootExpression = expression;
- _enableDataValidation = enableDataValidation;
- }
-
- static BindingExpressionVisitor()
- {
- AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) })!;
- CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) })!;
- }
-
- public static List BuildNodes(Expression> expression, bool enableDataValidation)
+ ///
+ /// Builds a compiled binding path from a lambda expression.
+ ///
+ /// The output type of the binding expression.
+ ///
+ /// The lambda expression to parse and convert into a binding path.
+ ///
+ ///
+ /// A representing the binding path.
+ ///
+ ///
+ /// Thrown when the expression contains unsupported operations or invalid syntax for binding
+ /// expressions.
+ ///
+ public static CompiledBindingPath BuildPath(Expression> expression)
{
- var visitor = new BindingExpressionVisitor(expression, enableDataValidation);
+ var visitor = new BindingExpressionVisitor(expression);
visitor.Visit(expression);
- return visitor._nodes;
+ return visitor._builder.Build();
}
protected override Expression VisitBinary(BinaryExpression node)
@@ -49,33 +61,64 @@ internal class BindingExpressionVisitor : ExpressionVisitor
// 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 }));
+ return Visit(Expression.MakeIndex(node.Left, null, [node.Right]));
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitIndex(IndexExpression node)
{
- if (node.Indexer == AvaloniaObjectIndexer)
+ if (node.Indexer == BindingExpressionVisitorMembers.AvaloniaObjectIndexer)
{
var property = GetValue(node.Arguments[0]);
- return Add(node.Object, node, new AvaloniaPropertyAccessorNode(property));
+ return Add(node.Object, node, x => x.Property(property, CreateAvaloniaPropertyAccessor));
}
- else
+ else if (node.Object?.Type.IsArray == true)
+ {
+ var indexes = node.Arguments.Select(GetValue).ToArray();
+ return Add(node.Object, node, x => x.ArrayElement(indexes, node.Type));
+ }
+ else if (node.Indexer?.GetMethod is not null &&
+ node.Arguments.Count == 1 &&
+ node.Arguments[0].Type == typeof(int))
{
- return Add(node.Object, node, new ExpressionTreeIndexerNode(node));
+ var getMethod = node.Indexer.GetMethod;
+ var setMethod = node.Indexer.SetMethod;
+ var index = GetValue(node.Arguments[0]);
+ var info = new ClrPropertyInfo(
+ CommonPropertyNames.IndexerName,
+ x => getMethod.Invoke(x, new object[] { index }),
+ setMethod is not null ? (o, v) => setMethod.Invoke(o, new[] { index, v }) : null,
+ getMethod.ReturnType);
+ return Add(node.Object, node, x => x.Property(
+ info,
+ (weakRef, propInfo) => CreateIndexerPropertyAccessor(weakRef, propInfo, index)));
}
+ else if (node.Indexer?.GetMethod is not null)
+ {
+ var getMethod = node.Indexer.GetMethod;
+ var setMethod = node.Indexer?.SetMethod;
+ var indexes = node.Arguments.Select(GetValue).ToArray();
+ var info = new ClrPropertyInfo(
+ CommonPropertyNames.IndexerName,
+ x => getMethod.Invoke(x, indexes),
+ setMethod is not null ? (o, v) => setMethod.Invoke(o, indexes.Append(v).ToArray()) : null,
+ getMethod.ReturnType);
+ return Add(node.Object, node, x => x.Property(
+ info,
+ CreateInpcPropertyAccessor));
+ }
+
+ throw new ExpressionParseException(0, $"Invalid indexer in binding expression: {node.NodeType}.");
}
protected override Expression VisitMember(MemberExpression node)
{
- switch (node.Member.MemberType)
+ return node.Member.MemberType switch
{
- case MemberTypes.Property:
- return Add(node.Expression, node, new DynamicPluginPropertyAccessorNode(node.Member.Name, acceptsNull: false));
- default:
- throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
- }
+ MemberTypes.Property => AddPropertyNode(node),
+ _ => throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."),
+ };
}
protected override Expression VisitMethodCall(MethodCallExpression node)
@@ -90,20 +133,43 @@ internal class BindingExpressionVisitor : ExpressionVisitor
else if (method.Name == MultiDimensionalArrayGetterMethodName &&
node.Object is not null)
{
- var expression = Expression.MakeIndex(node.Object, null, node.Arguments);
- return Add(node.Object, node, new ExpressionTreeIndexerNode(expression));
+ var indexes = node.Arguments.Select(GetValue).ToArray();
+ return Add(node.Object, node, x => x.ArrayElement(indexes, node.Type));
}
else if (method.Name.StartsWith(StreamBindingExtensions.StreamBindingName) &&
method.DeclaringType == typeof(StreamBindingExtensions))
{
var instance = node.Method.IsStatic ? node.Arguments[0] : node.Object;
- Add(instance, node, new DynamicPluginStreamNode());
- return node;
+ var instanceType = instance?.Type;
+ var genericArgs = method.GetGenericArguments();
+ var genericArg = genericArgs.Length > 0 ? genericArgs[0] : typeof(object);
+
+ if (instanceType == typeof(Task) ||
+ (instanceType?.IsGenericType == true &&
+ instanceType.GetGenericTypeDefinition() == typeof(Task<>) &&
+ genericArg.IsAssignableFrom(instanceType.GetGenericArguments()[0])))
+ {
+ var builderMethod = typeof(CompiledBindingPathBuilder)
+ .GetMethod(nameof(CompiledBindingPathBuilder.StreamTask))!
+ .MakeGenericMethod(genericArg);
+ return Add(instance, node, x => builderMethod.Invoke(x, null));
+ }
+ else if (typeof(IObservable<>).MakeGenericType(genericArg).IsAssignableFrom(instance?.Type))
+ {
+ var builderMethod = typeof(CompiledBindingPathBuilder)
+ .GetMethod(nameof(CompiledBindingPathBuilder.StreamObservable))!
+ .MakeGenericMethod(genericArg);
+ return Add(instance, node, x => builderMethod.Invoke(x, null));
+ }
}
- else if (method == CreateDelegateMethod)
+ else if (method == BindingExpressionVisitorMembers.CreateDelegateMethod)
{
- var accessor = new DynamicPluginPropertyAccessorNode(GetValue(node.Object!).Name, acceptsNull: false);
- return Add(node.Arguments[1], node, accessor);
+ var methodInfo = GetValue(node.Object!);
+ var delegateType = GetValue(node.Arguments[0]);
+ return Add(node.Arguments[1], node, x => x.Method(
+ methodInfo.MethodHandle,
+ delegateType.TypeHandle,
+ acceptsNull: false));
}
throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType}.{node.Method.Name}'.");
@@ -120,20 +186,26 @@ internal class BindingExpressionVisitor : ExpressionVisitor
{
if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool))
{
- return Add(node.Operand, node, new LogicalNotNode());
+ return Add(node.Operand, node, x => x.Not());
}
else if (node.NodeType == ExpressionType.Convert)
{
- if (node.Operand.Type.IsAssignableFrom(node.Type))
+ // Allow reference type casts (both upcasts and downcasts) but reject value type conversions
+ if (!node.Type.IsValueType && !node.Operand.Type.IsValueType &&
+ (node.Type.IsAssignableFrom(node.Operand.Type) || node.Operand.Type.IsAssignableFrom(node.Type)))
{
- // Ignore inheritance casts
- return _head = base.VisitUnary(node);
+ var castMethod = typeof(CompiledBindingPathBuilder)
+ .GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))!
+ .MakeGenericMethod(node.Type);
+ return Add(node.Operand, node, x => castMethod.Invoke(x, null));
}
}
else if (node.NodeType == ExpressionType.TypeAs)
{
- // Ignore as operator.
- return _head = base.VisitUnary(node);
+ var castMethod = typeof(CompiledBindingPathBuilder)
+ .GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))!
+ .MakeGenericMethod(node.Type);
+ return Add(node.Operand, node, x => castMethod.Invoke(x, null));
}
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
@@ -146,7 +218,7 @@ internal class BindingExpressionVisitor : ExpressionVisitor
protected override CatchBlock VisitCatchBlock(CatchBlock node)
{
- throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions.");
+ throw new ExpressionParseException(0, "Catch blocks are not allowed in binding expressions.");
}
protected override Expression VisitConditional(ConditionalExpression node)
@@ -156,17 +228,17 @@ internal class BindingExpressionVisitor : ExpressionVisitor
protected override Expression VisitDynamic(DynamicExpression node)
{
- throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions.");
+ 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.");
+ 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.");
+ throw new ExpressionParseException(0, "Goto expressions are not supported in binding expressions.");
}
protected override Expression VisitInvocation(InvocationExpression node)
@@ -191,7 +263,7 @@ internal class BindingExpressionVisitor : ExpressionVisitor
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{
- throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions.");
+ throw new ExpressionParseException(0, "Member assignments not supported in binding expressions.");
}
protected override Expression VisitSwitch(SwitchExpression node)
@@ -209,17 +281,69 @@ internal class BindingExpressionVisitor : ExpressionVisitor
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
- private Expression Add(Expression? instance, Expression expression, ExpressionNode node)
+ private Expression Add(Expression? instance, Expression expression, Action build)
{
var visited = Visit(instance);
+
if (visited != _head)
+ {
throw new ExpressionParseException(
- 0,
+ 0,
$"Unable to parse '{expression}': expected an instance of '{_head}' but got '{visited}'.");
- _nodes.Add(node);
+ }
+
+ build(_builder);
return _head = expression;
}
+ private Expression AddPropertyNode(MemberExpression node)
+ {
+ // Check if it's an AvaloniaProperty accessed via CLR wrapper
+ if (typeof(AvaloniaObject).IsAssignableFrom(node.Expression?.Type) &&
+ AvaloniaPropertyRegistry.Instance.FindRegistered(node.Expression.Type, node.Member.Name) is { } avaloniaProperty)
+ {
+ return Add(
+ node.Expression,
+ node,
+ x => x.Property(avaloniaProperty, CreateAvaloniaPropertyAccessor));
+ }
+ else
+ {
+ var property = (PropertyInfo)node.Member;
+ var info = new ClrPropertyInfo(
+ property.Name,
+ CreateGetter(property),
+ CreateSetter(property),
+ property.PropertyType);
+ return Add(node.Expression, node, x => x.Property(info, CreateInpcPropertyAccessor));
+ }
+ }
+
+ private static Func? CreateGetter(PropertyInfo info)
+ {
+ if (info.GetMethod == null)
+ return null;
+ var target = Expression.Parameter(typeof(object), "target");
+ return Expression.Lambda>(
+ Expression.Convert(Expression.Call(Expression.Convert(target, info.DeclaringType!), info.GetMethod),
+ typeof(object)),
+ target)
+ .Compile();
+ }
+
+ private static Action? CreateSetter(PropertyInfo info)
+ {
+ if (info.SetMethod == null)
+ return null;
+ var target = Expression.Parameter(typeof(object), "target");
+ var value = Expression.Parameter(typeof(object), "value");
+ return Expression.Lambda>(
+ Expression.Call(Expression.Convert(target, info.DeclaringType!), info.SetMethod,
+ Expression.Convert(value, info.SetMethod.GetParameters()[0].ParameterType)),
+ target, value)
+ .Compile();
+ }
+
private static T GetValue(Expression expr)
{
if (expr is ConstantExpression constant)
@@ -232,4 +356,168 @@ internal class BindingExpressionVisitor : ExpressionVisitor
var type = method.DeclaringType;
return type?.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method);
}
+
+ // Accessor factory methods
+ private static IPropertyAccessor CreateInpcPropertyAccessor(WeakReference target, IPropertyInfo property)
+ => new InpcPropertyAccessor(target, property);
+
+ private static IPropertyAccessor CreateAvaloniaPropertyAccessor(WeakReference target, IPropertyInfo property)
+ => new AvaloniaPropertyAccessor(
+ new WeakReference((AvaloniaObject?)(target.TryGetTarget(out var o) ? o : null)),
+ (AvaloniaProperty)property);
+
+ private static IPropertyAccessor CreateIndexerPropertyAccessor(WeakReference target, IPropertyInfo property, int argument)
+ => new IndexerAccessor(target, property, argument);
+
+ // Accessor implementations
+ private class AvaloniaPropertyAccessor : PropertyAccessorBase, IWeakEventSubscriber
+ {
+ private readonly WeakReference _reference;
+ private readonly AvaloniaProperty _property;
+
+ public AvaloniaPropertyAccessor(WeakReference reference, AvaloniaProperty property)
+ {
+ _reference = reference ?? throw new ArgumentNullException(nameof(reference));
+ _property = property ?? throw new ArgumentNullException(nameof(property));
+ }
+
+ public override Type PropertyType => _property.PropertyType;
+ public override object? Value => _reference.TryGetTarget(out var instance) ? instance?.GetValue(_property) : null;
+
+ public override bool SetValue(object? value, BindingPriority priority)
+ {
+ if (!_property.IsReadOnly && _reference.TryGetTarget(out var instance))
+ {
+ instance.SetValue(_property, value, priority);
+ return true;
+ }
+ return false;
+ }
+
+ public void OnEvent(object? sender, WeakEvent ev, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == _property)
+ PublishValue(Value);
+ }
+
+ protected override void SubscribeCore()
+ {
+ if (_reference.TryGetTarget(out var reference) && reference is not null)
+ {
+ PublishValue(reference.GetValue(_property));
+ WeakEvents.AvaloniaPropertyChanged.Subscribe(reference, this);
+ }
+ }
+
+ protected override void UnsubscribeCore()
+ {
+ if (_reference.TryGetTarget(out var reference) && reference is not null)
+ WeakEvents.AvaloniaPropertyChanged.Unsubscribe(reference, this);
+ }
+ }
+
+ private class InpcPropertyAccessor : PropertyAccessorBase, IWeakEventSubscriber
+ {
+ protected readonly WeakReference _reference;
+ private readonly IPropertyInfo _property;
+
+ public InpcPropertyAccessor(WeakReference reference, IPropertyInfo property)
+ {
+ _reference = reference ?? throw new ArgumentNullException(nameof(reference));
+ _property = property ?? throw new ArgumentNullException(nameof(property));
+ }
+
+ public override Type PropertyType => _property.PropertyType;
+ public override object? Value => _reference.TryGetTarget(out var o) ? _property.Get(o) : null;
+
+ public override bool SetValue(object? value, BindingPriority priority)
+ {
+ if (_property.CanSet && _reference.TryGetTarget(out var o))
+ {
+ _property.Set(o, value);
+ SendCurrentValue();
+ return true;
+ }
+ return false;
+ }
+
+ public void OnEvent(object? sender, WeakEvent ev, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
+ SendCurrentValue();
+ }
+
+ protected override void SubscribeCore()
+ {
+ SendCurrentValue();
+ if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc)
+ WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
+ }
+
+ protected override void UnsubscribeCore()
+ {
+ if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc)
+ WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
+ }
+
+ protected void SendCurrentValue()
+ {
+ try
+ {
+ PublishValue(Value);
+ }
+ catch (Exception e)
+ {
+ PublishValue(new BindingNotification(e, BindingErrorType.Error));
+ }
+ }
+ }
+
+ private class IndexerAccessor : InpcPropertyAccessor, IWeakEventSubscriber
+ {
+ private readonly int _index;
+
+ public IndexerAccessor(WeakReference target, IPropertyInfo basePropertyInfo, int argument)
+ : base(target, basePropertyInfo)
+ {
+ _index = argument;
+ }
+
+ protected override void SubscribeCore()
+ {
+ base.SubscribeCore();
+ if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc)
+ WeakEvents.CollectionChanged.Subscribe(incc, this);
+ }
+
+ protected override void UnsubscribeCore()
+ {
+ base.UnsubscribeCore();
+ if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc)
+ WeakEvents.CollectionChanged.Unsubscribe(incc, this);
+ }
+
+ public void OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs args)
+ {
+ if (ShouldNotifyListeners(args))
+ SendCurrentValue();
+ }
+
+ private bool ShouldNotifyListeners(NotifyCollectionChangedEventArgs e)
+ {
+ return e.Action switch
+ {
+ NotifyCollectionChangedAction.Add => _index >= e.NewStartingIndex,
+ NotifyCollectionChangedAction.Remove => _index >= e.OldStartingIndex,
+ NotifyCollectionChangedAction.Replace => _index >= e.NewStartingIndex &&
+ _index < e.NewStartingIndex + e.NewItems!.Count,
+ NotifyCollectionChangedAction.Move => (_index >= e.NewStartingIndex &&
+ _index < e.NewStartingIndex + e.NewItems!.Count) ||
+ (_index >= e.OldStartingIndex &&
+ _index < e.OldStartingIndex + e.OldItems!.Count),
+ NotifyCollectionChangedAction.Reset => true,
+ _ => false
+ };
+ }
+ }
}
diff --git a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitorMembers.cs b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitorMembers.cs
new file mode 100644
index 0000000000..82163f89e6
--- /dev/null
+++ b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitorMembers.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Reflection;
+
+namespace Avalonia.Data.Core.Parsers;
+
+///
+/// Stores reflection members used by outside of the
+/// generic class to avoid duplication for each generic instantiation.
+///
+internal static class BindingExpressionVisitorMembers
+{
+ static BindingExpressionVisitorMembers()
+ {
+ AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty(CommonPropertyNames.IndexerName, [typeof(AvaloniaProperty)])!;
+ CreateDelegateMethod = typeof(MethodInfo).GetMethod(nameof(MethodInfo.CreateDelegate), [typeof(Type), typeof(object)])!;
+ }
+
+ public static readonly PropertyInfo AvaloniaObjectIndexer;
+ public static readonly MethodInfo CreateDelegateMethod;
+}
diff --git a/tests/Avalonia.Base.UnitTests/Data/CompiledBindingTests_Create.cs b/tests/Avalonia.Base.UnitTests/Data/CompiledBindingTests_Create.cs
new file mode 100644
index 0000000000..0dcce969df
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Data/CompiledBindingTests_Create.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Globalization;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Data;
+
+public class CompiledBindingTests_Create
+{
+ [Fact]
+ public void Create_Should_Create_Binding_With_Simple_Property()
+ {
+ var binding = CompiledBinding.Create(vm => vm.StringProperty);
+
+ Assert.NotNull(binding);
+ Assert.NotNull(binding.Path);
+ Assert.Equal("StringProperty", binding.Path.ToString());
+ Assert.Equal(AvaloniaProperty.UnsetValue, binding.Source);
+ Assert.Equal(BindingMode.Default, binding.Mode);
+ }
+
+ [Fact]
+ public void Create_Should_Create_Binding_With_Source()
+ {
+ var source = new TestViewModel { StringProperty = "Test" };
+ var binding = CompiledBinding.Create(
+ vm => vm.StringProperty,
+ source: source);
+
+ Assert.NotNull(binding);
+ Assert.NotNull(binding.Path);
+ Assert.Equal("StringProperty", binding.Path.ToString());
+ Assert.Same(source, binding.Source);
+ }
+
+ [Fact]
+ public void Create_Should_Apply_Converter()
+ {
+ var converter = new TestConverter();
+ var binding = CompiledBinding.Create(
+ vm => vm.StringProperty,
+ converter: converter);
+
+ Assert.Same(converter, binding.Converter);
+ }
+
+ [Fact]
+ public void Create_Should_Apply_Mode()
+ {
+ var binding = CompiledBinding.Create(
+ vm => vm.StringProperty,
+ mode: BindingMode.TwoWay);
+
+ Assert.Equal(BindingMode.TwoWay, binding.Mode);
+ }
+
+ [Fact]
+ public void Create_Should_Work_With_Nested_Properties()
+ {
+ var binding = CompiledBinding.Create(
+ vm => vm.Child!.StringProperty);
+
+ Assert.NotNull(binding);
+ Assert.NotNull(binding.Path);
+ Assert.Equal("Child.StringProperty", binding.Path.ToString());
+ }
+
+ [Fact]
+ public void Create_Should_Work_With_Indexer()
+ {
+ var binding = CompiledBinding.Create(
+ vm => vm.Items[0]);
+
+ Assert.NotNull(binding);
+ Assert.NotNull(binding.Path);
+ Assert.Equal("Items[0]", binding.Path.ToString());
+ }
+
+ [Fact]
+ public void Binding_Should_Work_When_Applied_To_Control()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new TextBlock();
+ var viewModel = new TestViewModel { StringProperty = "Hello" };
+ var binding = CompiledBinding.Create(
+ vm => vm.StringProperty,
+ source: viewModel);
+
+ target.Bind(TextBlock.TextProperty, binding);
+
+ Assert.Equal("Hello", target.Text);
+ }
+ }
+
+ [Fact]
+ public void Binding_Should_Update_When_Source_Property_Changes()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new TextBlock();
+ var viewModel = new TestViewModel { StringProperty = "Initial" };
+ var binding = CompiledBinding.Create(
+ vm => vm.StringProperty,
+ source: viewModel);
+
+ target.Bind(TextBlock.TextProperty, binding);
+ Assert.Equal("Initial", target.Text);
+
+ viewModel.StringProperty = "Updated";
+ Assert.Equal("Updated", target.Text);
+ }
+ }
+
+ [Fact]
+ public void Binding_Should_Use_DataContext_When_No_Source_Specified()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new TextBlock();
+ var viewModel = new TestViewModel { StringProperty = "FromDataContext" };
+ var binding = CompiledBinding.Create(vm => vm.StringProperty);
+
+ target.DataContext = viewModel;
+ target.Bind(TextBlock.TextProperty, binding);
+
+ Assert.Equal("FromDataContext", target.Text);
+ }
+ }
+
+ private class TestViewModel : NotifyingBase
+ {
+ private string? _stringProperty;
+ private TestViewModel? _child;
+
+ public string? StringProperty
+ {
+ get => _stringProperty;
+ set { _stringProperty = value; RaisePropertyChanged(); }
+ }
+
+ public TestViewModel? Child
+ {
+ get => _child;
+ set { _child = value; RaisePropertyChanged(); }
+ }
+
+ public string?[] Items { get; set; } = Array.Empty();
+ }
+
+ private class TestConverter : IValueConverter
+ {
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => value?.ToString()?.ToUpper();
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => value?.ToString()?.ToLower();
+ }
+}
diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
index 251be3fc27..6302b68b08 100644
--- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
@@ -95,7 +95,7 @@ public abstract partial class BindingExpressionTests
var target = new TargetClass { DataContext = dataContext };
var nodes = new List();
var fallback = fallbackValue.HasValue ? fallbackValue.Value : AvaloniaProperty.UnsetValue;
- var path = CompiledBindingPathFromExpressionBuilder.Build(expression, enableDataValidation);
+ var path = BindingExpressionVisitor.BuildPath(expression);
if (relativeSource is not null && relativeSource.Mode is not RelativeSourceMode.Self)
throw new NotImplementedException();
diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/CompiledBindingPathFromExpressionBuilder.cs b/tests/Avalonia.Base.UnitTests/Data/Core/CompiledBindingPathFromExpressionBuilder.cs
deleted file mode 100644
index d3623fde80..0000000000
--- a/tests/Avalonia.Base.UnitTests/Data/Core/CompiledBindingPathFromExpressionBuilder.cs
+++ /dev/null
@@ -1,313 +0,0 @@
-using System;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Threading.Tasks;
-using Avalonia.Data;
-using Avalonia.Data.Core;
-using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
-
-#nullable enable
-
-namespace Avalonia.Base.UnitTests.Data.Core;
-
-internal class CompiledBindingPathFromExpressionBuilder : ExpressionVisitor
-{
- private static readonly PropertyInfo AvaloniaObjectIndexer;
- private static readonly MethodInfo CreateDelegateMethod;
- private static readonly string IndexerGetterName = "get_Item";
- private const string MultiDimensionalArrayGetterMethodName = "Get";
- private readonly bool _enableDataValidation;
- private readonly LambdaExpression _rootExpression;
- private readonly CompiledBindingPathBuilder _builder = new();
- private Expression? _head;
-
- public CompiledBindingPathFromExpressionBuilder(LambdaExpression expression, bool enableDataValidation)
- {
- _rootExpression = expression;
- _enableDataValidation = enableDataValidation;
- }
-
- static CompiledBindingPathFromExpressionBuilder()
- {
- AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) })!;
- CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) })!;
- }
-
- public static CompiledBindingPath Build(Expression> expression, bool enableDataValidation)
- {
- var visitor = new CompiledBindingPathFromExpressionBuilder(expression, enableDataValidation);
- visitor.Visit(expression);
- return visitor._builder.Build();
- }
-
- 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(node.Arguments[0]);
- return Add(node.Object, node, x => x.Property(property, PropertyInfoAccessorFactory.CreateAvaloniaPropertyAccessor));
- }
- else if (node.Object?.Type.IsArray == true)
- {
- var indexes = node.Arguments.Select(GetValue).ToArray();
- return Add(node.Object, node, x => x.ArrayElement(indexes, node.Type));
- }
- else if (node.Indexer?.GetMethod is not null &&
- node.Arguments.Count == 1 &&
- node.Arguments[0].Type == typeof(int))
- {
- var getMethod = node.Indexer.GetMethod;
- var setMethod = node.Indexer.SetMethod;
- var index = GetValue(node.Arguments[0]);
- var info = new ClrPropertyInfo(
- CommonPropertyNames.IndexerName,
- x => getMethod.Invoke(x, new object[] { index }),
- setMethod is not null ? (o, v) => setMethod.Invoke(o, new[] { v }) : null,
- getMethod.ReturnType);
- return Add(node.Object, node, x => x.Property(
- info,
- (x, i) => PropertyInfoAccessorFactory.CreateIndexerPropertyAccessor(x, i, index)));
- }
- else if (node.Indexer?.GetMethod is not null)
- {
- var getMethod = node.Indexer.GetMethod;
- var setMethod = node.Indexer?.SetMethod;
- var indexes = node.Arguments.Select(GetValue).ToArray();
- var info = new ClrPropertyInfo(
- CommonPropertyNames.IndexerName,
- x => getMethod.Invoke(x, indexes),
- setMethod is not null ? (o, v) => setMethod.Invoke(o, indexes.Append(v).ToArray()) : null,
- getMethod.ReturnType);
- return Add(node.Object, node, x => x.Property(
- info,
- PropertyInfoAccessorFactory.CreateInpcPropertyAccessor));
- }
-
- throw new ExpressionParseException(0, $"Invalid indexer in binding expression: {node.NodeType}.");
- }
-
- protected override Expression VisitMember(MemberExpression node)
- {
- if (node.Member.MemberType != MemberTypes.Property)
- throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
-
- if (typeof(AvaloniaObject).IsAssignableFrom(node.Expression?.Type) &&
- AvaloniaPropertyRegistry.Instance.FindRegistered(node.Expression.Type, node.Member.Name) is { } avaloniaProperty)
- {
- return Add(
- node.Expression,
- node,
- x => x.Property(avaloniaProperty, PropertyInfoAccessorFactory.CreateAvaloniaPropertyAccessor));
- }
- else
- {
- var property = (PropertyInfo)node.Member;
- var info = new ClrPropertyInfo(
- property.Name,
- CreateGetter(property),
- CreateSetter(property),
- property.PropertyType);
- return Add(node.Expression, node, x => x.Property(info, PropertyInfoAccessorFactory.CreateInpcPropertyAccessor));
- }
- }
-
- 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 = node.Arguments.Select(GetValue).ToArray();
- return Add(node.Object, node, x => x.ArrayElement(indexes, node.Type));
- }
- 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;
-
- if (typeof(Task<>).MakeGenericType(genericArg).IsAssignableFrom(instance?.Type))
- {
- var builderMethod = typeof(CompiledBindingPathBuilder)
- .GetMethod(nameof(CompiledBindingPathBuilder.StreamTask))!
- .MakeGenericMethod(genericArg);
- return Add(instance, node, x => builderMethod.Invoke(x, null));
- }
- else if (typeof(IObservable<>).MakeGenericType(genericArg).IsAssignableFrom(instance?.Type))
- {
- var builderMethod = typeof(CompiledBindingPathBuilder)
- .GetMethod(nameof(CompiledBindingPathBuilder.StreamObservable))!
- .MakeGenericMethod(genericArg);
- return Add(instance, node, x => builderMethod.Invoke(x, null));
- }
- }
-
- 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))
- {
- return Add(node.Operand, node, x => x.Not());
- }
- 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, Action build)
- {
- var visited = Visit(instance);
- if (visited != _head)
- throw new ExpressionParseException(
- 0,
- $"Unable to parse '{expression}': expected an instance of '{_head}' but got '{visited}'.");
- build(_builder);
- return _head = expression;
- }
-
- private static Func? CreateGetter(PropertyInfo info)
- {
- if (info.GetMethod == null)
- return null;
- var target = Expression.Parameter(typeof(object), "target");
- return Expression.Lambda>(
- Expression.Convert(Expression.Call(Expression.Convert(target, info.DeclaringType!), info.GetMethod),
- typeof(object)),
- target)
- .Compile();
- }
-
- private static Action? CreateSetter(PropertyInfo info)
- {
- if (info.SetMethod == null)
- return null;
- var target = Expression.Parameter(typeof(object), "target");
- var value = Expression.Parameter(typeof(object), "value");
- return Expression.Lambda>(
- Expression.Call(Expression.Convert(target, info.DeclaringType!), info.SetMethod,
- Expression.Convert(value, info.SetMethod.GetParameters()[0].ParameterType)),
- target, value)
- .Compile();
- }
-
- private static T GetValue(Expression expr)
- {
- if (expr is ConstantExpression constant)
- return (T)constant.Value!;
- return Expression.Lambda>(expr).Compile(preferInterpretation: true)();
- }
-
- private static PropertyInfo? TryGetPropertyFromMethod(MethodInfo method)
- {
- var type = method.DeclaringType;
- return type?.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method);
- }
-}
diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/BindingExpressionVisitorExtensions.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/BindingExpressionVisitorExtensions.cs
new file mode 100644
index 0000000000..6f3d3474f5
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/BindingExpressionVisitorExtensions.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using Avalonia.Data.Core.ExpressionNodes;
+using Avalonia.Data.Core.Parsers;
+
+namespace Avalonia.Base.UnitTests.Data.Core.Parsers;
+
+///
+/// Test extensions for BindingExpressionVisitor tests.
+///
+internal static class BindingExpressionVisitorExtensions
+{
+ ///
+ /// Builds a list of binding expression nodes from a lambda expression.
+ /// This is a test helper method - production code should use BuildPath() instead.
+ ///
+ public static List BuildNodes(Expression> expression)
+ {
+ var path = BindingExpressionVisitor.BuildPath(expression);
+ var nodes = new List();
+ path.BuildExpression(nodes, out var _);
+ return nodes;
+ }
+}
diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/BindingExpressionVisitorTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/BindingExpressionVisitorTests.cs
new file mode 100644
index 0000000000..ddd0f87c04
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/BindingExpressionVisitorTests.cs
@@ -0,0 +1,528 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+using Avalonia.Data.Core;
+using Avalonia.Data.Core.ExpressionNodes;
+using Avalonia.Data.Core.Parsers;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Data.Core.Parsers
+{
+ public class BindingExpressionVisitorTests
+ {
+ [Fact]
+ public void BuildNodes_Should_Parse_Simple_Property()
+ {
+ Expression> expr = x => x.StringProperty;
+
+ var nodes = BuildNodes(expr);
+
+ var node = Assert.Single(nodes);
+ var propertyNode = Assert.IsType(node);
+ Assert.Equal("StringProperty", propertyNode.PropertyName);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Property_Chain()
+ {
+ Expression> expr = x => x.Child!.StringProperty;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var firstNode = Assert.IsType(nodes[0]);
+ Assert.Equal("Child", firstNode.PropertyName);
+
+ var secondNode = Assert.IsType(nodes[1]);
+ Assert.Equal("StringProperty", secondNode.PropertyName);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Long_Property_Chain()
+ {
+ Expression> expr = x => x.Child!.Child!.StringProperty;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(3, nodes.Count);
+ Assert.All(nodes, n => Assert.IsType(n));
+ Assert.Equal("Child", ((PropertyAccessorNode)nodes[0]).PropertyName);
+ Assert.Equal("Child", ((PropertyAccessorNode)nodes[1]).PropertyName);
+ Assert.Equal("StringProperty", ((PropertyAccessorNode)nodes[2]).PropertyName);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Indexer()
+ {
+ Expression> expr = x => x.IndexedProperty![0];
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("IndexedProperty", propertyNode.PropertyName);
+
+ Assert.IsType(nodes[1]); // List indexer, not array
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Array_Index()
+ {
+ Expression> expr = x => x.ArrayProperty![0];
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("ArrayProperty", propertyNode.PropertyName);
+
+ Assert.IsType(nodes[1]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Multi_Dimensional_Array()
+ {
+ Expression> expr = x => x.MultiDimensionalArray![0, 1];
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("MultiDimensionalArray", propertyNode.PropertyName);
+
+ Assert.IsType(nodes[1]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_AvaloniaProperty_Access()
+ {
+ Expression> expr = x => x[StyledElement.DataContextProperty];
+
+ var nodes = BuildNodes(expr);
+
+ var node = Assert.Single(nodes);
+ var avaloniaPropertyNode = Assert.IsType(node);
+ Assert.Equal("DataContext", avaloniaPropertyNode.PropertyName); // AvaloniaProperty accessed as property
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_AvaloniaProperty_Access_In_Chain()
+ {
+ Expression> expr = x => x.StyledChild![StyledElement.DataContextProperty];
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("StyledChild", propertyNode.PropertyName);
+
+ var avaloniaPropertyNode = Assert.IsType(nodes[1]);
+ Assert.Equal("DataContext", avaloniaPropertyNode.PropertyName); // AvaloniaProperty accessed as property
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Logical_Not()
+ {
+ Expression> expr = x => !x.BoolProperty;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("BoolProperty", propertyNode.PropertyName);
+
+ Assert.IsType(nodes[1]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Logical_Not_In_Chain()
+ {
+ Expression> expr = x => !x.Child!.BoolProperty;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(3, nodes.Count);
+ Assert.IsType(nodes[0]);
+ Assert.IsType(nodes[1]);
+ Assert.IsType(nodes[2]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Task_StreamBinding()
+ {
+ Expression> expr = x => x.TaskProperty!.StreamBinding();
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("TaskProperty", propertyNode.PropertyName);
+
+ Assert.IsType(nodes[1]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Observable_StreamBinding()
+ {
+ Expression> expr = x => x.ObservableProperty!.StreamBinding();
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("ObservableProperty", propertyNode.PropertyName);
+
+ Assert.IsType(nodes[1]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Void_Task_StreamBinding()
+ {
+ Expression> expr = x => x.VoidTaskProperty!.StreamBinding();
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("VoidTaskProperty", propertyNode.PropertyName);
+
+ Assert.IsType(nodes[1]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Create_Node_For_Upcast()
+ {
+ // Upcasts (derived to base) create a cast node
+ Expression> expr = x => (TestClass)x;
+
+ var nodes = BuildNodes(expr);
+
+ var node = Assert.Single(nodes);
+ Assert.IsType(node);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Create_Node_For_Upcast_In_Property_Chain()
+ {
+ // Cast creates a node, then property access creates another
+ Expression> expr = x => ((TestClass)x).StringProperty;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+ Assert.IsType(nodes[0]);
+ var propertyNode = Assert.IsType(nodes[1]);
+ Assert.Equal("StringProperty", propertyNode.PropertyName);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Create_Node_For_Downcast()
+ {
+ // Downcasts (base to derived) create a cast node - the binding system will handle runtime errors
+ Expression> expr = x => (DerivedTestClass)x;
+
+ var nodes = BuildNodes(expr);
+
+ var node = Assert.Single(nodes);
+ Assert.IsType(node);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Create_Node_For_Downcast_In_Property_Chain()
+ {
+ // Practical example: casting to access derived type properties
+ Expression> expr = x => ((DerivedTestClass)x.Child!).DerivedProperty;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(3, nodes.Count);
+ var childNode = Assert.IsType(nodes[0]);
+ Assert.Equal("Child", childNode.PropertyName);
+ Assert.IsType(nodes[1]);
+ var derivedNode = Assert.IsType(nodes[2]);
+ Assert.Equal("DerivedProperty", derivedNode.PropertyName);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Throw_For_Value_Type_Cast()
+ {
+ // Value type conversions should throw
+ Expression> expr = x => (long)x.IntProperty;
+
+ var ex = Assert.Throws(() =>
+ BuildNodes(expr));
+
+ Assert.Contains("Invalid expression type", ex.Message);
+ Assert.Contains("Convert", ex.Message);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Create_Nodes_For_Casting_Through_Object()
+ {
+ // Casting through object creates cast nodes
+ Expression> expr = x => (string)(object)x.StringProperty!;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(3, nodes.Count);
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("StringProperty", propertyNode.PropertyName);
+ Assert.IsType(nodes[1]); // cast to object
+ Assert.IsType(nodes[2]); // cast to string
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Create_Node_For_TypeAs_Operator()
+ {
+ // TypeAs operator creates a cast node
+ Expression> expr = x => x.Child as object;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("Child", propertyNode.PropertyName);
+ Assert.IsType(nodes[1]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Throw_For_Addition_Operator()
+ {
+ Expression> expr = x => x.IntProperty + 1;
+
+ var ex = Assert.Throws(() =>
+ BuildNodes(expr));
+
+ Assert.Contains("Invalid expression type", ex.Message);
+ Assert.Contains("Add", ex.Message);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Throw_For_Subtraction_Operator()
+ {
+ Expression> expr = x => x.IntProperty - 1;
+
+ var ex = Assert.Throws(() =>
+ BuildNodes(expr));
+
+ Assert.Contains("Invalid expression type", ex.Message);
+ Assert.Contains("Subtract", ex.Message);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Throw_For_Multiplication_Operator()
+ {
+ Expression> expr = x => x.IntProperty * 2;
+
+ var ex = Assert.Throws(() =>
+ BuildNodes(expr));
+
+ Assert.Contains("Invalid expression type", ex.Message);
+ Assert.Contains("Multiply", ex.Message);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Throw_For_Equality_Operator()
+ {
+ Expression> expr = x => x.IntProperty == 42;
+
+ var ex = Assert.Throws(() =>
+ BuildNodes(expr));
+
+ Assert.Contains("Invalid expression type", ex.Message);
+ Assert.Contains("Equal", ex.Message);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Throw_For_Conditional_Expression()
+ {
+ Expression> expr = x => x.BoolProperty ? "true" : "false";
+
+ var ex = Assert.Throws(() =>
+ BuildNodes(expr));
+
+ Assert.Contains("Invalid expression type", ex.Message);
+ Assert.Contains("Conditional", ex.Message);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Throw_For_Method_Call_That_Is_Not_Indexer_Or_StreamBinding()
+ {
+ Expression> expr = x => x.StringProperty!.ToUpper();
+
+ var ex = Assert.Throws(() =>
+ BuildNodes(expr));
+
+ Assert.Contains("Invalid method call", ex.Message);
+ Assert.Contains("ToUpper", ex.Message);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Handle_Unary_Plus_Operator()
+ {
+ // Unary plus is typically optimized away by the C# compiler and doesn't appear in the
+ // expression tree, so it doesn't throw an exception.
+ Expression> expr = x => +x.IntProperty;
+
+ var nodes = BuildNodes(expr);
+
+ var node = Assert.Single(nodes);
+ var propertyNode = Assert.IsType(node);
+ Assert.Equal("IntProperty", propertyNode.PropertyName);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Throw_For_Unary_Minus_Operator()
+ {
+ Expression> expr = x => -x.IntProperty;
+
+ var ex = Assert.Throws(() =>
+ BuildNodes(expr));
+
+ Assert.Contains("Invalid expression type", ex.Message);
+ Assert.Contains("Negate", ex.Message);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Chained_Indexers()
+ {
+ Expression> expr = x => x.NestedIndexedProperty![0]![1];
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(3, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("NestedIndexedProperty", propertyNode.PropertyName);
+
+ Assert.IsType(nodes[1]); // List indexer
+ Assert.IsType(nodes[2]); // List indexer
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Property_After_Indexer()
+ {
+ Expression> expr = x => x.IndexedProperty![0]!.StringProperty;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(3, nodes.Count);
+ Assert.IsType(nodes[0]);
+ Assert.IsType(nodes[1]); // List indexer
+ Assert.IsType(nodes[2]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Indexer_With_String_Key()
+ {
+ Expression> expr = x => x.DictionaryProperty!["key"];
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+
+ var propertyNode = Assert.IsType(nodes[0]);
+ Assert.Equal("DictionaryProperty", propertyNode.PropertyName);
+
+ Assert.IsType(nodes[1]); // Dictionary indexer
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Indexer_With_Variable_Key()
+ {
+ var key = "test";
+ Expression> expr = x => x.DictionaryProperty![key];
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(2, nodes.Count);
+ Assert.IsType(nodes[0]);
+ Assert.IsType(nodes[1]); // Dictionary indexer
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_StreamBinding_In_Property_Chain()
+ {
+ Expression> expr = x => x.Child!.TaskProperty!.StreamBinding();
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(3, nodes.Count);
+ Assert.IsType(nodes[0]);
+ Assert.IsType(nodes[1]);
+ Assert.IsType(nodes[2]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Logical_Not_After_StreamBinding()
+ {
+ Expression> expr = x => !x.BoolTaskProperty!.StreamBinding();
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(3, nodes.Count);
+ Assert.IsType(nodes[0]);
+ Assert.IsType(nodes[1]);
+ Assert.IsType(nodes[2]);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Handle_Empty_Expression()
+ {
+ Expression> expr = x => x;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Empty(nodes);
+ }
+
+ [Fact]
+ public void BuildNodes_Should_Parse_Multiple_Logical_Not_Operators()
+ {
+ Expression> expr = x => !!x.BoolProperty;
+
+ var nodes = BuildNodes(expr);
+
+ Assert.Equal(3, nodes.Count);
+ Assert.IsType(nodes[0]);
+ Assert.IsType(nodes[1]);
+ Assert.IsType(nodes[2]);
+ }
+
+ public class TestClass
+ {
+ public string? StringProperty { get; set; }
+ public int IntProperty { get; set; }
+ public bool BoolProperty { get; set; }
+ public TestClass? Child { get; set; }
+ public StyledElement? StyledChild { get; set; }
+ public string?[]? ArrayProperty { get; set; }
+ public string?[,]? MultiDimensionalArray { get; set; }
+ public List? IndexedProperty { get; set; }
+ public List>? NestedIndexedProperty { get; set; }
+ public Dictionary? DictionaryProperty { get; set; }
+ public Task? TaskProperty { get; set; }
+ public Task? VoidTaskProperty { get; set; }
+ public Task? BoolTaskProperty { get; set; }
+ public IObservable? ObservableProperty { get; set; }
+ }
+
+ public class DerivedTestClass : TestClass
+ {
+ public string? DerivedProperty { get; set; }
+ }
+
+ private static List BuildNodes(Expression> expression)
+ => BindingExpressionVisitorExtensions.BuildNodes(expression);
+ }
+}
diff --git a/tests/Avalonia.LeakTests/BindingExpressionExtensions.cs b/tests/Avalonia.LeakTests/BindingExpressionExtensions.cs
new file mode 100644
index 0000000000..98ec67e31b
--- /dev/null
+++ b/tests/Avalonia.LeakTests/BindingExpressionExtensions.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq.Expressions;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Data.Core;
+using Avalonia.Data.Core.ExpressionNodes;
+using Avalonia.Data.Core.Parsers;
+using Avalonia.Utilities;
+
+namespace Avalonia.LeakTests;
+
+///
+/// Test extensions for creating BindingExpression instances from lambda expressions.
+///
+internal static class BindingExpressionExtensions
+{
+ [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
+ [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
+ public static BindingExpression CreateBindingExpression(
+ TIn source,
+ Expression> expression,
+ IValueConverter? converter = null,
+ CultureInfo? converterCulture = null,
+ object? converterParameter = null,
+ bool enableDataValidation = false,
+ Optional fallbackValue = default,
+ BindingMode mode = BindingMode.OneWay,
+ BindingPriority priority = BindingPriority.LocalValue,
+ object? targetNullValue = null,
+ bool allowReflection = true)
+ where TIn : class?
+ {
+ var path = BindingExpressionVisitor.BuildPath(expression);
+ var nodes = new List();
+ path.BuildExpression(nodes, out var _);
+ var fallback = fallbackValue.HasValue ? fallbackValue.Value : AvaloniaProperty.UnsetValue;
+
+ return new BindingExpression(
+ source,
+ nodes,
+ fallback,
+ converter: converter,
+ converterCulture: converterCulture,
+ converterParameter: converterParameter,
+ enableDataValidation: enableDataValidation,
+ mode: mode,
+ priority: priority,
+ targetNullValue: targetNullValue,
+ targetTypeConverter: allowReflection ?
+ TargetTypeConverter.GetReflectionConverter() :
+ TargetTypeConverter.GetDefaultConverter());
+ }
+}
diff --git a/tests/Avalonia.LeakTests/BindingExpressionTests.cs b/tests/Avalonia.LeakTests/BindingExpressionTests.cs
index 5a6eddc869..c5b38e209f 100644
--- a/tests/Avalonia.LeakTests/BindingExpressionTests.cs
+++ b/tests/Avalonia.LeakTests/BindingExpressionTests.cs
@@ -1,6 +1,10 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.Globalization;
+using System.Linq.Expressions;
using Avalonia.Collections;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.UnitTests;
using Xunit;
@@ -16,7 +20,7 @@ namespace Avalonia.LeakTests
{
var list = new AvaloniaList { "foo", "bar" };
var source = new { Foo = list };
- var target = BindingExpression.Create(source, o => o.Foo);
+ var target = CreateBindingExpression(source, o => o.Foo);
target.ToObservable().Subscribe(_ => { });
return new WeakReference(list);
@@ -37,7 +41,7 @@ namespace Avalonia.LeakTests
{
var list = new AvaloniaList { "foo", "bar" };
var source = new { Foo = list };
- var target = BindingExpression.Create(source, o => o.Foo, enableDataValidation: true);
+ var target = CreateBindingExpression(source, o => o.Foo, enableDataValidation: true);
target.ToObservable().Subscribe(_ => { });
return new WeakReference(list);
@@ -58,7 +62,7 @@ namespace Avalonia.LeakTests
{
var indexer = new NonIntegerIndexer();
var source = new { Foo = indexer };
- var target = BindingExpression.Create(source, o => o.Foo);
+ var target = CreateBindingExpression(source, o => o.Foo);
target.ToObservable().Subscribe(_ => { });
return new WeakReference(indexer);
@@ -79,7 +83,7 @@ namespace Avalonia.LeakTests
{
var methodBound = new MethodBound();
var source = new { Foo = methodBound };
- var target = BindingExpression.Create(source, o => (Action)o.Foo.A);
+ var target = CreateBindingExpression(source, o => (Action)o.Foo.A);
target.ToObservable().Subscribe(_ => { });
return new WeakReference(methodBound);
}
@@ -92,6 +96,34 @@ namespace Avalonia.LeakTests
Assert.False(weakSource.IsAlive);
}
+ private static BindingExpression CreateBindingExpression(
+ TIn source,
+ Expression> expression,
+ IValueConverter? converter = null,
+ CultureInfo? converterCulture = null,
+ object? converterParameter = null,
+ bool enableDataValidation = false,
+ Optional fallbackValue = default,
+ BindingMode mode = BindingMode.OneWay,
+ BindingPriority priority = BindingPriority.LocalValue,
+ object? targetNullValue = null,
+ bool allowReflection = true)
+ where TIn : class?
+ {
+ return BindingExpressionExtensions.CreateBindingExpression(
+ source,
+ expression,
+ converter,
+ converterCulture,
+ converterParameter,
+ enableDataValidation,
+ fallbackValue,
+ mode,
+ priority,
+ targetNullValue,
+ allowReflection);
+ }
+
private class MethodBound
{
public void A() { }