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; /// /// A binding which does not use reflection to access members. /// public class CompiledBinding : BindingBase { /// /// Initializes a new instance of the class. /// public CompiledBinding() { } /// /// Initializes a new instance of the class. /// /// 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 /// AvaloniaProperty access: x => x[MyProperty] /// /// [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Expression statically preserves members used in binding expressions.")] 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. /// /// /// There is no delay when the source is updated via /// or . Nor is there a delay when /// is active and a new source object is provided. /// public int Delay { get; set; } /// /// Gets or sets the to use. /// public IValueConverter? Converter { get; set; } /// /// Gets or sets the culture in which to evaluate the converter. /// /// The default value is null. /// /// If this property is not set then will be used. /// [TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))] public CultureInfo? ConverterCulture { get; set; } /// /// Gets or sets a parameter to pass to . /// public object? ConverterParameter { get; set; } /// /// Gets or sets the value to use when the binding is unable to produce a value. /// public object? FallbackValue { get; set; } = AvaloniaProperty.UnsetValue; /// /// Gets or sets the binding mode. /// public BindingMode Mode { get; set; } /// /// Gets or sets the binding path. /// [ConstructorArgument("path")] public CompiledBindingPath? Path { get; set; } /// /// Gets or sets the binding priority. /// public BindingPriority Priority { get; set; } /// /// Gets or sets the source for the binding. /// public object? Source { get; set; } = AvaloniaProperty.UnsetValue; /// /// Gets or sets the string format. /// public string? StringFormat { get; set; } /// /// Gets or sets the value to use when the binding result is null. /// public object? TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue; /// /// Gets or sets a value that determines the timing of binding source updates for /// and bindings. /// public UpdateSourceTrigger UpdateSourceTrigger { get; set; } internal WeakReference? DefaultAnchor { get; set; } internal WeakReference? NameScope { get; set; } internal override BindingExpressionBase CreateInstance( AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor) { var enableDataValidation = targetProperty?.GetMetadata(target).EnableDataValidation ?? false; var nodes = new List(); 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); } }