A cross-platform UI framework for .NET
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
10 KiB

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;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
using Avalonia.Metadata;
namespace Avalonia.Data;
/// <summary>
/// A binding which does not use reflection to access members.
/// </summary>
public class CompiledBinding : BindingBase
{
/// <summary>
/// Initializes a new instance of the <see cref="CompiledBinding"/> class.
/// </summary>
public CompiledBinding() { }
/// <summary>
/// Initializes a new instance of the <see cref="CompiledBinding"/> class.
/// </summary>
/// <param name="path">The binding path.</param>
public CompiledBinding(CompiledBindingPath path) => Path = path;
/// <summary>
/// Creates a <see cref="CompiledBinding"/> from a lambda expression.
/// </summary>
/// <typeparam name="TIn">The input type of the binding expression.</typeparam>
/// <typeparam name="TOut">The output type of the binding expression.</typeparam>
/// <param name="expression">
/// The lambda expression representing the binding path
/// (e.g., <c>vm => vm.PropertyName</c>).
/// </param>
/// <param name="source"
/// >The source object for the binding. If null, uses the target's DataContext.
/// </param>
/// <param name="converter">
/// Optional value converter to transform values between source and target.
/// </param>
/// <param name="mode">
/// The binding mode. Default is <see cref="BindingMode.Default"/> which resolves to the
/// property's default binding mode.
/// </param>
/// <param name="priority">The binding priority.</param>
/// <param name="converterCulture">The culture in which to evaluate the converter.</param>
/// <param name="converterParameter">A parameter to pass to the converter.</param>
/// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value.
/// </param>
/// <param name="stringFormat">The string format for the binding result.</param>
/// <param name="targetNullValue">The value to use when the binding result is null.</param>
/// <param name="updateSourceTrigger">
/// The timing of binding source updates for TwoWay/OneWayToSource bindings.
/// </param>
/// <param name="delay">
/// The amount of time, in milliseconds, to wait before updating the binding source.
/// </param>
/// <returns>
/// A configured <see cref="CompiledBinding"/> instance ready to be applied to a property.
/// </returns>
/// <exception cref="ExpressionParseException">
/// Thrown when the expression contains unsupported operations or invalid syntax for binding
/// expressions.
/// </exception>
/// <remarks>
/// This builds a <see cref="CompiledBinding"/> 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:
/// <list type="bullet">
/// <item>Property access: <c>x => x.Property</c></item>
/// <item>Nested properties: <c>x => x.Property.Nested</c></item>
/// <item>Indexers: <c>x => x.Items[0]</c></item>
/// <item>Type casts: <c>x => ((DerivedType)x).Property</c></item>
/// <item>Logical NOT: <c>x => !x.BoolProperty</c></item>
/// <item>Stream bindings: <c>x => x.TaskProperty</c> (Task/Observable)</item>
/// <item>AvaloniaProperty access: <c>x => x[MyProperty]</c></item>
/// </list>
/// </remarks>
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
public static CompiledBinding Create<TIn, TOut>(
Expression<Func<TIn, TOut>> 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<TIn>.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
};
}
/// <summary>
/// Gets or sets the amount of time, in milliseconds, to wait before updating the binding
/// source after the value on the target changes.
/// </summary>
/// <remarks>
/// There is no delay when the source is updated via <see cref="UpdateSourceTrigger.LostFocus"/>
/// or <see cref="BindingExpressionBase.UpdateSource"/>. Nor is there a delay when
/// <see cref="BindingMode.OneWayToSource"/> is active and a new source object is provided.
/// </remarks>
public int Delay { get; set; }
/// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary>
public IValueConverter? Converter { get; set; }
/// <summary>
/// Gets or sets the culture in which to evaluate the converter.
/// </summary>
/// <value>The default value is null.</value>
/// <remarks>
/// If this property is not set then <see cref="CultureInfo.CurrentCulture"/> will be used.
/// </remarks>
[TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))]
public CultureInfo? ConverterCulture { get; set; }
/// <summary>
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object? ConverterParameter { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding is unable to produce a value.
/// </summary>
public object? FallbackValue { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
/// <summary>
/// Gets or sets the binding path.
/// </summary>
[ConstructorArgument("path")]
public CompiledBindingPath? Path { get; set; }
/// <summary>
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the source for the binding.
/// </summary>
public object? Source { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets the string format.
/// </summary>
public string? StringFormat { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding result is null.
/// </summary>
public object? TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets a value that determines the timing of binding source updates for
/// <see cref="BindingMode.TwoWay"/> and <see cref="BindingMode.OneWayToSource"/> bindings.
/// </summary>
public UpdateSourceTrigger UpdateSourceTrigger { get; set; }
internal WeakReference? DefaultAnchor { get; set; }
internal WeakReference<INameScope?>? NameScope { get; set; }
internal override BindingExpressionBase CreateInstance(
AvaloniaObject target,
AvaloniaProperty? targetProperty,
object? anchor)
{
var enableDataValidation = targetProperty?.GetMetadata(target).EnableDataValidation ?? false;
var nodes = new List<ExpressionNode>();
var isRooted = false;
Path?.BuildExpression(nodes, out isRooted);
// If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self,
// #elementName etc.) then we need to add a data context source node.
if (Source == AvaloniaProperty.UnsetValue && !isRooted)
nodes.Insert(0, ExpressionNodeFactory.CreateDataContext(targetProperty));
// If the first node is an ISourceNode then allow it to select the source; otherwise
// use the binding source if specified, falling back to the target.
var source = nodes?.Count > 0 && nodes[0] is SourceNode sn
? sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target)
: Source != AvaloniaProperty.UnsetValue ? Source : target;
var (mode, trigger) = ResolveDefaultsFromMetadata(target, targetProperty);
return new BindingExpression(
source,
nodes,
FallbackValue,
delay: TimeSpan.FromMilliseconds(Delay),
converter: Converter,
converterCulture: ConverterCulture,
converterParameter: ConverterParameter,
enableDataValidation: enableDataValidation,
mode: mode,
priority: Priority,
stringFormat: StringFormat,
targetNullValue: TargetNullValue,
targetProperty: targetProperty,
targetTypeConverter: TargetTypeConverter.GetDefaultConverter(),
updateSourceTrigger: trigger);
}
private (BindingMode, UpdateSourceTrigger) ResolveDefaultsFromMetadata(
AvaloniaObject target,
AvaloniaProperty? targetProperty)
{
var mode = Mode;
var trigger = UpdateSourceTrigger == UpdateSourceTrigger.Default ?
UpdateSourceTrigger.PropertyChanged : UpdateSourceTrigger;
if (mode == BindingMode.Default)
{
if (targetProperty?.GetMetadata(target) is { } metadata)
mode = metadata.DefaultBindingMode;
else
mode = BindingMode.OneWay;
}
return (mode, trigger);
}
}