using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq.Expressions; using System.Text; using Avalonia.Data.Converters; using Avalonia.Data.Core.ExpressionNodes; using Avalonia.Data.Core.Parsers; using Avalonia.Logging; namespace Avalonia.Data.Core; /// /// A binding expression which accepts and produces (possibly boxed) object values. /// /// /// A represents a untyped binding which has been /// instantiated on an object. /// internal class BindingExpression : IObservable, IObserver, IDescription, IDisposable { private static readonly WeakReference NullReference = new(null); private readonly WeakReference? _source; private readonly WeakReference _target; private readonly BindingMode _mode; private readonly IReadOnlyList _nodes; private readonly AvaloniaProperty? _targetProperty; private readonly TargetTypeConverter? _targetTypeConverter; private readonly bool _enableDataValidation; private IObserver? _observer; private WeakReference? _value; private UncommonFields? _uncommon; /// /// Initializes a new instance of the class. /// /// The source from which the value will be read. /// The nodes representing the binding path. /// /// The fallback value. Pass for no fallback. /// /// The converter to use. /// The converter parameter. /// /// Whether data validation should be enabled for the binding. /// /// The binding mode. /// The format string to use. /// The target to which the value will be written. /// The null target value. /// The target property. /// /// A final type converter to be run on the produced value. /// public BindingExpression( object? source, IReadOnlyList nodes, object? fallbackValue, IValueConverter? converter = null, object? converterParameter = null, bool enableDataValidation = false, BindingMode mode = BindingMode.OneWay, string? stringFormat = null, AvaloniaObject? target = null, object? targetNullValue = null, AvaloniaProperty? targetProperty = null, TargetTypeConverter? targetTypeConverter = null) { if (mode == BindingMode.Default) throw new ArgumentException("Binding mode cannot be Default.", nameof(mode)); if (target is null && mode is BindingMode.TwoWay or BindingMode.OneWayToSource) throw new ArgumentException("Target cannot be null for TwoWay or OneWayToSource bindings.", nameof(target)); if (targetProperty is null && mode is BindingMode.TwoWay or BindingMode.OneWayToSource) throw new ArgumentException("Target property cannot be null for TwoWay or OneWayToSource bindings.", nameof(target)); if (source == AvaloniaProperty.UnsetValue) source = null; _source = new(source); _target = new(target); _targetProperty = targetProperty; _mode = mode; _nodes = nodes; _targetTypeConverter = targetTypeConverter; _enableDataValidation = enableDataValidation; if (converter is not null || converterParameter is not null || fallbackValue != AvaloniaProperty.UnsetValue || (targetNullValue is not null && targetNullValue != AvaloniaProperty.UnsetValue) || !string.IsNullOrWhiteSpace(stringFormat)) { _uncommon = new() { _converter = converter, _converterParameter = converterParameter, _fallbackValue = fallbackValue, _targetNullValue = targetNullValue ?? AvaloniaProperty.UnsetValue, _stringFormat = stringFormat switch { string s when string.IsNullOrWhiteSpace(s) => null, string s when !s.Contains('{') => $"{{0:{stringFormat}}}", _ => stringFormat, }, }; } IPropertyAccessorNode? leafAccessor = null; for (var i = 0; i < nodes.Count; ++i) { var node = nodes[i]; node.SetOwner(this, i); if (node is IPropertyAccessorNode n) leafAccessor = n; } if (enableDataValidation) leafAccessor?.EnableDataValidation(); } public string Description { get { var b = new StringBuilder(); LeafNode.BuildString(b, _nodes); return b.ToString(); } } public Type? SourceType => (LeafNode as ISettableNode)?.ValueType; public Type TargetType => _targetProperty?.PropertyType ?? typeof(object); public IValueConverter? Converter => _uncommon?._converter; public object? ConverterParameter => _uncommon?._converterParameter; public object? FallbackValue => _uncommon is not null ? _uncommon._fallbackValue : AvaloniaProperty.UnsetValue; public object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue; public ExpressionNode LeafNode => _nodes[_nodes.Count - 1]; public string? StringFormat => _uncommon?._stringFormat; /// /// Writes the specified value to the binding source if possible. /// /// The value to write. /// /// True if the value could be written to the binding source; otherwise false. /// public bool SetValue(object? value) { if (_nodes.Count == 0 || LeafNode is not ISettableNode setter || setter.ValueType is not { } type) return false; if (Converter is not null) value = Converter.ConvertBack(value, type, ConverterParameter, CultureInfo.CurrentCulture); if (value == BindingOperations.DoNothing) return true; // If the value is the same as the last value, then don't set the value. if (_value is not null && _value.TryGetTarget(out var lastValue) && Equals(value, lastValue)) return true; // If the value is null and the last value was null, then don't set the value. This is a // separate step from the above because WeakReference.TryGetTarget() returns false for // null references. if (value is null && _value == NullReference) return true; // Use the target type converter to convert the value to the target type if necessary. if (_targetTypeConverter is not null) { if (_targetTypeConverter.TryConvert(value, type, CultureInfo.CurrentCulture, out var converted)) { value = converted; } else if (FallbackValue != AvaloniaProperty.UnsetValue) { value = FallbackValue; } else if (_enableDataValidation) { var valueString = value?.ToString() ?? "(null)"; var valueTypeName = value?.GetType().FullName ?? "null"; var ex = new InvalidCastException( $"Could not convert '{valueString}' ({valueTypeName}) to {type}."); _observer?.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError)); return false; } else { return false; } } try { return setter.WriteValueToSource(value, _nodes); } catch { return false; } } /// /// 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 parameter. /// Whether data validation should be enabled for the binding. /// The fallback value. /// The binding mode. /// The target to which the value will be written. /// The null target value. /// The target property. [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] public static BindingExpression Create( TIn source, Expression> expression, IValueConverter? converter = null, object? converterParameter = null, bool enableDataValidation = false, Optional fallbackValue = default, BindingMode mode = BindingMode.OneWay, AvaloniaObject? target = null, object? targetNullValue = null, AvaloniaProperty? targetProperty = null) 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, converterParameter: converterParameter, enableDataValidation: enableDataValidation, mode: mode, target: target, targetNullValue: targetNullValue, targetProperty: targetProperty, targetTypeConverter: TargetTypeConverter.GetReflectionConverter()); } /// /// Implements the disposable returned by . /// void IDisposable.Dispose() { if (_observer is null) return; _observer = null; Stop(); } IDisposable IObservable.Subscribe(IObserver observer) { if (_observer is not null) throw new InvalidOperationException( $"An {nameof(BindingExpression)} may only have a single subscriber."); _observer = observer ?? throw new ArgumentNullException(nameof(observer)); Start(); return this; } void IObserver.OnCompleted() { } void IObserver.OnError(Exception error) { } void IObserver.OnNext(object? value) => SetValue(value); /// /// Called by an belonging to this binding when its /// changes. /// /// The . /// The . internal void OnNodeValueChanged(int nodeIndex, object? value) { if (value is BindingNotification notification && notification.ErrorType == BindingErrorType.Error && notification.Error is not null && ShouldLogError(out var target)) { // Log any errors the arrive via a node value change. This is mainly to make sure that // errors which come from property accessors get logged. Log(target, notification.Error.Message, CalculateErrorPoint(nodeIndex)); } if (nodeIndex == _nodes.Count - 1) { // The leaf node has changed. If the binding mode is not OneWayToSource, publish the // value to the target. if (_mode != BindingMode.OneWayToSource) PublishValue(); // If the binding mode is OneTime, then stop the binding. if (_mode == BindingMode.OneTime) Stop(); } else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null) { // When the binding mode is OneWayToSource, we need to write the value to the source // when the object holding the source property changes; this is node before the leaf // node. First update the leaf node's source, then write the value to its property. _nodes[nodeIndex + 1].SetSource(value); WriteTargetValueToSource(); } else if (value is null) { OnNodeError(nodeIndex, "Value is null."); } else { _nodes[nodeIndex + 1].SetSource(value); } } /// /// Called by an belonging to this binding when an error occurs /// reading its value. /// /// /// The or -1 if the source is null. /// /// The error message. internal void OnNodeError(int nodeIndex, string error) { _value = null; // Set the source of all nodes after the one that errored to null. This needs to be done // for each node individually because setting the source to null will not result in // OnNodeValueChanged or OnNodeError being called. for (var i = nodeIndex + 1; i < _nodes.Count; ++i) _nodes[i].SetSource(null); if (_observer is null || _mode == BindingMode.OneWayToSource) return; var errorPoint = CalculateErrorPoint(nodeIndex); if (ShouldLogError(out var target)) Log(target, error, errorPoint); var e = new BindingChainException(error, Description, errorPoint.ToString()); _observer.OnNext(new BindingNotification( e, BindingErrorType.Error, ConvertFallback(FallbackValue, nameof(FallbackValue)))); } private string CalculateErrorPoint(int nodeIndex) { // Build a string describing the binding chain up to the node that errored. var result = new StringBuilder(); if (nodeIndex >= 0) _nodes[nodeIndex].BuildString(result); else result.Append("(source)"); return result.ToString(); } private void Log(AvaloniaObject target, string error, LogEventLevel level = LogEventLevel.Warning) { if (!Logger.TryGet(level, LogArea.Binding, out var log)) return; log.Log( target, "An error occurred binding {Property} to {Expression}: {Message}", (object?)_targetProperty ?? "(unknown)", Description, error); } private void Log(AvaloniaObject target, string error, string errorPoint, LogEventLevel level = LogEventLevel.Warning) { if (!Logger.TryGet(level, LogArea.Binding, out var log)) return; log.Log( target, "An error occurred binding {Property} to {Expression} at {ExpressionErrorPoint}: {Message}", (object?)_targetProperty ?? "(unknown)", Description, errorPoint, error); } private bool ShouldLogError([NotNullWhen(true)] out AvaloniaObject? target) { if (!_target.TryGetTarget(out target)) return false; if (_nodes.Count > 0 && _nodes[0] is SourceNode sourceNode) return sourceNode.ShouldLogErrors(target); return true; } private void Start() { if (_observer is null) return; if (_source?.TryGetTarget(out var source) == true) { if (_nodes.Count > 0) _nodes[0].SetSource(source); else _observer.OnNext(source); if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && _target.TryGetTarget(out var target) && _targetProperty is not null) { if (_mode is BindingMode.OneWayToSource) SetValue(target.GetValue(_targetProperty)); target.PropertyChanged += OnTargetPropertyChanged; } } else { OnNodeError(-1, "Binding Source is null."); } } private void Stop() { foreach (var node in _nodes) node.Reset(); if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && _target.TryGetTarget(out var target)) { target.PropertyChanged -= OnTargetPropertyChanged; } } private void PublishValue() { if (_observer is null) return; // The value can be a simple value or a BindingNotification. As we move through this method // we'll keep `notification` updated with the value and current error state by calling // `UpdateAndUnwrap`. var valueOrNotification = _nodes.Count > 0 ? _nodes[_nodes.Count - 1].Value : null; var value = BindingNotification.ExtractValue(valueOrNotification); var notification = valueOrNotification as BindingNotification; var isTargetNullValue = false; // All values other than DoNothing should be passed to the converter. if (value != BindingOperations.DoNothing && Converter is { } converter) { value = UpdateAndUnwrap( Convert( converter, ConverterParameter, value, _targetProperty?.PropertyType ?? typeof(object)), ref notification); } // Check this here as the converter may return DoNothing. if (value == BindingOperations.DoNothing) return; // TargetNullValue only applies when the value is null: UnsetValue indicates that there // was a binding error so we don't want to use TargetNullValue in that case. if (value is null && TargetNullValue != AvaloniaProperty.UnsetValue) { value = UpdateAndUnwrap(ConvertFallback(TargetNullValue, nameof(TargetNullValue)), ref notification); isTargetNullValue = true; } // If we have a value, try to convert it to the target type. if (value != AvaloniaProperty.UnsetValue) { if (StringFormat is { } stringFormat && (TargetType == typeof(object) || TargetType == typeof(string)) && !isTargetNullValue) { // The string format applies if we're targeting a type that can accept a string // and the value isn't the TargetNullValue. value = string.Format(CultureInfo.CurrentCulture, stringFormat, value); } else if (_targetTypeConverter is not null && value is not null) { // Otherwise, if we have a target type converter, convert the value to the target type. value = UpdateAndUnwrap(ConvertFrom(_targetTypeConverter, value), ref notification); } } // FallbackValue applies if the result from the binding, converter or target type converter // is UnsetValue. if (value == AvaloniaProperty.UnsetValue && FallbackValue != AvaloniaProperty.UnsetValue) value = UpdateAndUnwrap(ConvertFallback(FallbackValue, nameof(FallbackValue)), ref notification); // Store the value and publish the notification/value to the observer. _value = value is null ? NullReference : new(value); _observer.OnNext(notification ?? value); } private void WriteTargetValueToSource() { if (_mode != BindingMode.OneWayToSource) return; if (_target.TryGetTarget(out var target) && _targetProperty is not null && target.GetValue(_targetProperty) is var value && !Equals(value, LeafNode.Value)) { SetValue(value); } } private void OnSourceChanged(object? source) { if (_nodes.Count > 0) _nodes[0].SetSource(source); } private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _targetProperty) { SetValue(e.NewValue); } } private static object? Convert( IValueConverter converter, object? converterParameter, object? value, Type targetType) { try { return converter.Convert(value, targetType, converterParameter, CultureInfo.CurrentCulture); } catch (Exception e) { var valueString = value?.ToString() ?? "(null)"; var valueTypeName = value?.GetType().FullName ?? "null"; var ex = new InvalidCastException( $"Could not convert '{valueString}' ({valueTypeName}) to {targetType} using '{converter}'.", e); return new BindingNotification(ex, BindingErrorType.Error); } } private object? ConvertFallback(object? fallback, string fallbackName) { if (_targetTypeConverter is null || TargetType == typeof(object) || fallback == AvaloniaProperty.UnsetValue) return fallback; if (_targetTypeConverter.TryConvert(fallback, TargetType, CultureInfo.CurrentCulture, out var result)) return result; if (_target.TryGetTarget(out var target)) Log(target, $"Could not convert {fallbackName} '{fallback}' to '{TargetType}'.", LogEventLevel.Error); return AvaloniaProperty.UnsetValue; } private object? ConvertFrom(TargetTypeConverter? converter, object value) { if (converter is null || _targetProperty is null) return value; var targetType = _targetProperty.PropertyType; if (converter.TryConvert(value, targetType, CultureInfo.CurrentCulture, out var result)) return result; var valueString = value?.ToString() ?? "(null)"; var valueTypeName = value?.GetType().FullName ?? "null"; var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{targetType}'."; if (ShouldLogError(out var target)) Log(target, message, LogEventLevel.Warning); return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error); } private static object? UpdateAndUnwrap(object? value, ref BindingNotification? notification) { if (value is BindingNotification n) { value = n.Value; if (n.Error is not null) { if (notification is null) notification = n; else notification.AddError(n.Error, n.ErrorType); } } notification?.SetValue(value); return value; } private class UncommonFields { public IValueConverter? _converter; public object? _converterParameter; public object? _fallbackValue; public string? _stringFormat; public object? _targetNullValue; } }