diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index bc21765a3c..7a0ae7d1d9 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.PropertyStore; @@ -608,6 +609,14 @@ namespace Avalonia internal ValueStore GetValueStore() => _values; internal IReadOnlyList? GetInheritanceChildren() => _inheritanceChildren; + internal IDisposable Bind( + AvaloniaProperty property, + BindingExpression expression, + BindingPriority priority) + { + return property.RouteBind(this, expression, priority); + } + /// /// Called to update the validation state for properties for which data validation is /// enabled. diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 5ab52cfde9..48993ee402 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -542,6 +542,14 @@ namespace Avalonia /// The value. internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value); + /// + /// Routes an untyped SetDirectValueUnchecked call to a typed call. + /// + /// The object instance. + /// The value. + internal virtual void RouteSetDirectValueUnchecked(AvaloniaObject o, object? value) => + throw new NotSupportedException(); + /// /// Routes an untyped Bind call to a typed call. /// @@ -553,6 +561,17 @@ namespace Avalonia IObservable source, BindingPriority priority); + /// + /// Routes an untyped Bind call to a typed call. + /// + /// The object instance. + /// The binding source. + /// The priority. + internal abstract IDisposable RouteBind( + AvaloniaObject o, + BindingExpression source, + BindingPriority priority); + /// /// Overrides the metadata for the property on the specified type. /// diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 6446f9f525..1dd845cded 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -26,7 +26,7 @@ namespace Avalonia.Data if (binding.Source is BindingExpression expression) { - return target.Bind(property, expression); + return target.Bind(property, expression, BindingPriority.LocalValue); } var mode = binding.Mode; diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index ac79d1eff7..6d2872fc82 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -415,6 +415,45 @@ namespace Avalonia.Data e); } + /// + /// Creates a from an object, handling the special values + /// , and + /// without type conversion. + /// + /// The untyped value. + /// The typed binding value. + internal static BindingValue FromUntypedStrict(object? value) + { + if (value == AvaloniaProperty.UnsetValue) + return Unset; + else if (value == BindingOperations.DoNothing) + return DoNothing; + + var type = BindingValueType.Value; + T? v = default; + Exception? error = null; + + if (value is BindingNotification n) + { + error = n.Error; + type = n.ErrorType switch + { + BindingErrorType.Error => BindingValueType.BindingError, + BindingErrorType.DataValidationError => BindingValueType.DataValidationError, + _ => BindingValueType.Value, + }; + + if (n.HasValue) + type |= BindingValueType.HasValue; + value = n.Value; + } + + if ((type & BindingValueType.HasValue) != 0) + v = (T)value!; + + return new BindingValue(type, v, error); + } + [Conditional("DEBUG")] private static void ValidateValue(T value) { diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.Observable.cs b/src/Avalonia.Base/Data/Core/BindingExpression.Observable.cs new file mode 100644 index 0000000000..121e455fb8 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/BindingExpression.Observable.cs @@ -0,0 +1,56 @@ +using System; + +namespace Avalonia.Data.Core; + +// Mostly for unit tests, BindingExpression implements IObservable and IObserver. +// We limit support to a single subscriber in this scenario. +internal partial class BindingExpression : IObservable, IObserver +{ + IDisposable IObservable.Subscribe(IObserver observer) + { + if (observer is null) + throw new ArgumentNullException(nameof(observer)); + if (_sink is not null) + throw new InvalidOperationException( + $"An {nameof(BindingExpression)} may only have a single subscriber."); + + _sink = new ObservableSink(observer); + Start(produceValue: true); + return this; + } + + void IObserver.OnCompleted() { } + void IObserver.OnError(Exception error) { } + void IObserver.OnNext(object? value) => SetValue(value); + + private class ObservableSink : IBindingExpressionSink + { + private IObserver _observer; + public ObservableSink(IObserver observer) => _observer = observer; + + public void OnChanged(BindingExpression instance, bool hasValueChanged, bool hasErrorChanged) + { + instance.GetDataValidationState(out var state, out var error); + + if (instance.IsDataValidationEnabled || error is not null) + { + BindingNotification notification; + + if (state.HasFlag(BindingValueType.BindingError) && error is not null) + notification = new(error, BindingErrorType.Error, instance.GetValue()); + else if (state.HasFlag(BindingValueType.DataValidationError) && error is not null) + notification = new(error, BindingErrorType.DataValidationError, instance.GetValue()); + else + notification = new(instance.GetValue()); + + _observer.OnNext(notification); + } + else if (hasValueChanged) + { + _observer.OnNext(instance.GetValue()); + } + } + + public void OnCompleted(BindingExpression instance) => _observer.OnCompleted(); + } +} diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.ValueEntry.cs b/src/Avalonia.Base/Data/Core/BindingExpression.ValueEntry.cs new file mode 100644 index 0000000000..40cbf88b1b --- /dev/null +++ b/src/Avalonia.Base/Data/Core/BindingExpression.ValueEntry.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.PropertyStore; +using Avalonia.Styling; + +namespace Avalonia.Data.Core; + +internal partial class BindingExpression : IValueEntry, ISetterInstance +{ + bool IValueEntry.HasValue + { + get + { + Start(produceValue: false); + return _value is not null; + } + } + + AvaloniaProperty IValueEntry.Property => _targetProperty ?? throw new Exception(); + + bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error) + { + GetDataValidationState(out state, out error); + return IsDataValidationEnabled; + } + + object? IValueEntry.GetValue() + { + Start(produceValue: false); + return GetValueOrDefault(); + } + + void IValueEntry.Unsubscribe() => Stop(); +} diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 61a48d80cf..d18f8fec15 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq.Expressions; @@ -8,7 +9,7 @@ using Avalonia.Data.Converters; using Avalonia.Data.Core.ExpressionNodes; using Avalonia.Data.Core.Parsers; using Avalonia.Logging; -using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; +using Avalonia.Threading; namespace Avalonia.Data.Core; @@ -19,21 +20,21 @@ namespace Avalonia.Data.Core; /// A represents a untyped binding which has been /// instantiated on an object. /// -internal class BindingExpression : IObservable, - IObserver, - IDescription, - IDisposable +internal partial class BindingExpression : IDescription, IDisposable { internal 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 bool _isRunning; + private BindingPriority _priority; + private bool _produceValue; + private IBindingExpressionSink? _sink; + private AvaloniaProperty? _targetProperty; private WeakReference? _value; + private BindingError? _error; private UncommonFields? _uncommon; /// @@ -89,7 +90,7 @@ internal class BindingExpression : IObservable, _mode = mode; _nodes = nodes; _targetTypeConverter = targetTypeConverter; - _enableDataValidation = enableDataValidation; + IsDataValidationEnabled = enableDataValidation; if (converter is not null || converterCulture is not null || @@ -138,16 +139,83 @@ internal class BindingExpression : IObservable, } } + public BindingPriority Priority => _priority; public Type? SourceType => (LeafNode as ISettableNode)?.ValueType; + public AvaloniaProperty? TargetProperty => _targetProperty; public Type TargetType => _targetProperty?.PropertyType ?? typeof(object); public IValueConverter? Converter => _uncommon?._converter; public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture; public object? ConverterParameter => _uncommon?._converterParameter; public object? FallbackValue => _uncommon is not null ? _uncommon._fallbackValue : AvaloniaProperty.UnsetValue; + public bool IsDataValidationEnabled { get; } + public bool HasDataValidationError => _error?.ErrorType == BindingValueType.DataValidationError; public object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue; public ExpressionNode LeafNode => _nodes[_nodes.Count - 1]; public string? StringFormat => _uncommon?._stringFormat; + /// + /// Gets the current value of the binding expression. + /// + /// + /// The current value or if the binding was unable + /// to read a value. + /// + /// + /// The binding expression has not been started. + /// + public object? GetValue() + { + if (!_isRunning) + throw new InvalidOperationException("BindingExpression has not been started."); + if (_value is null) + return AvaloniaProperty.UnsetValue; + else if (_value == NullReference) + return null; + else if (_value.TryGetTarget(out var value)) + return value; + else + return AvaloniaProperty.UnsetValue; + } + + /// + /// Gets the current value of the binding expression or the default value for the target property. + /// + /// + /// The current value or the target property default. + /// + /// + /// The binding expression has not been started. + /// + public object? GetValueOrDefault() + { + var result = GetValue(); + if (result == AvaloniaProperty.UnsetValue) + result = GetCachedDefaultValue(); + return result; + } + + /// + /// Gets the data validation state, if supported. + /// + /// The binding error state. + /// The current binding error, if any. + /// + /// True if the expression supports data validation, otherwise false. + /// + public void GetDataValidationState(out BindingValueType state, out Exception? error) + { + if (_error is not null) + { + state = _error.ErrorType; + error = _error.Exception; + } + else + { + state = BindingValueType.Value; + error = null; + } + } + /// /// Writes the specified value to the binding source if possible. /// @@ -177,13 +245,13 @@ internal class BindingExpression : IObservable, { value = FallbackValue; } - else if (_enableDataValidation) + else if (IsDataValidationEnabled) { 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)); + OnDataValidationError(ex); return false; } else @@ -206,6 +274,61 @@ internal class BindingExpression : IObservable, } } + /// + /// Initializes the binding expression with the specified subscriber and target property but + /// does not start it. + /// + /// The subscriber. + /// The target property. + /// The priority of the binding. + /// + /// is different to that passed in the constructor, if one was + /// passed there. + /// + public void Initialize( + IBindingExpressionSink subscriber, + AvaloniaProperty targetProperty, + BindingPriority priority) + { + if (_targetProperty is not null && _targetProperty != targetProperty) + throw new AvaloniaInternalException( + "StartAsLocalValueBinding was called with a property different to that passed in constructor."); + + _sink = subscriber; + _targetProperty = targetProperty; + _priority = priority; + } + + /// + /// Starts the binding expression with the specified subscriber and target property.. + /// + /// The subscriber. + /// The target property. + /// The priority of the binding. + public void Start( + IBindingExpressionSink subscriber, + AvaloniaProperty targetProperty, + BindingPriority priority) + { + Initialize(subscriber, targetProperty, priority); + Start(produceValue: true); + } + + /// + /// Terminates the binding. + /// + public void Dispose() + { + if (_sink is null) + return; + + Stop(); + + var sink = _sink; + _sink = null; + sink.OnCompleted(this); + } + /// /// Creates an from an expression tree. /// @@ -258,57 +381,26 @@ internal class BindingExpression : IObservable, TargetTypeConverter.GetReflectionConverter() : TargetTypeConverter.GetDefaultConverter()); } - - /// - /// 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) + /// + /// The data validation error associated with the current value, if any. + /// + internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataValidationError) { - 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)); - } + Debug.Assert(value is not BindingNotification); 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(); + UpdateAndPublishValue(value, dataValidationError); // If the binding mode is OneTime, then stop the binding. if (_mode == BindingMode.OneTime) @@ -319,7 +411,7 @@ internal class BindingExpression : IObservable, // 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); + _nodes[nodeIndex + 1].SetSource(value, dataValidationError); WriteTargetValueToSource(); } else if (value is null) @@ -328,7 +420,7 @@ internal class BindingExpression : IObservable, } else { - _nodes[nodeIndex + 1].SetSource(value); + _nodes[nodeIndex + 1].SetSource(value, dataValidationError); } } @@ -342,15 +434,13 @@ internal class BindingExpression : IObservable, /// 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); + _nodes[i].SetSource(null, null); - if (_observer is null || _mode == BindingMode.OneWayToSource) + if (_mode == BindingMode.OneWayToSource) return; var errorPoint = CalculateErrorPoint(nodeIndex); @@ -358,11 +448,21 @@ internal class BindingExpression : IObservable, 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)))); + // Clear the current value. + UpdateValue(AvaloniaProperty.UnsetValue, null, out var hasValueChanged, out _); + + // And store the error. + _error = new( + new BindingChainException(error, Description, errorPoint.ToString()), + BindingValueType.BindingError); + + PublishValue(hasValueChanged, hasErrorChanged: true); + } + + internal void OnDataValidationError(Exception error) + { + _error = new(error, BindingValueType.DataValidationError); + PublishValue(hasValueChanged: false, hasErrorChanged: true); } private string CalculateErrorPoint(int nodeIndex) @@ -378,6 +478,27 @@ internal class BindingExpression : IObservable, return result.ToString(); } + private object? GetCachedDefaultValue() + { + Debug.Assert(_targetProperty is not null); + + if (_uncommon?._isDefaultValueInitialized == true) + return _uncommon._defaultValue; + + if (_target.TryGetTarget(out var target)) + { + _uncommon ??= new(); + _uncommon._isDefaultValueInitialized = true; + + if (_targetProperty.IsDirect) + _uncommon._defaultValue = ((IDirectPropertyAccessor)_targetProperty).GetUnsetValue(target.GetType()); + else + _uncommon._defaultValue = ((IStyledPropertyAccessor)_targetProperty).GetDefaultValue(target.GetType()); + } + + return _uncommon?._defaultValue ?? AvaloniaProperty.UnsetValue; + } + private void Log(AvaloniaObject target, string error, LogEventLevel level = LogEventLevel.Warning) { if (!Logger.TryGet(level, LogArea.Binding, out var log)) @@ -414,17 +535,28 @@ internal class BindingExpression : IObservable, return true; } - private void Start() + private void Start(bool produceValue) { - if (_observer is null) + Debug.Assert(_sink is not null); + + if (_isRunning) return; + _isRunning = true; + _produceValue = produceValue; + if (_source?.TryGetTarget(out var source) == true) { if (_nodes.Count > 0) - _nodes[0].SetSource(source); + { + _nodes[0].SetSource(source, null); + } else - _observer.OnNext(source); + { + _value = new(source); + _error = null; + PublishValue(hasValueChanged: true, hasErrorChanged: false); + } if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && _target.TryGetTarget(out var target) && @@ -440,6 +572,8 @@ internal class BindingExpression : IObservable, { OnNodeError(-1, "Binding Source is null."); } + + _produceValue = true; } private void Stop() @@ -452,42 +586,50 @@ internal class BindingExpression : IObservable, { target.PropertyChanged -= OnTargetPropertyChanged; } + + _isRunning = false; + _value = null; } - private void PublishValue() + private void UpdateValue( + object? value, + Exception? dataValidationError, + out bool hasValueChanged, + out bool hasErrorChanged) { - 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; + var hadError = _error is not null; // 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); - } + value = Convert(converter, ConverterParameter, value, TargetType); // Check this here as the converter may return DoNothing. if (value == BindingOperations.DoNothing) + { + hasValueChanged = hasErrorChanged = false; + return; + } + + // Set the data validation error. + _error = dataValidationError is not null ? + new(dataValidationError, BindingValueType.DataValidationError) : + null; + + // If we have a data validation error and the value is Unset then we keep the + // current value. + if (dataValidationError is not null && value == AvaloniaProperty.UnsetValue) + { + hasValueChanged = false; + hasErrorChanged = true; 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); + value = ConvertFallback(TargetNullValue, nameof(TargetNullValue)); isTargetNullValue = true; } @@ -502,21 +644,63 @@ internal class BindingExpression : IObservable, // and the value isn't the TargetNullValue. value = string.Format(ConverterCulture, stringFormat, value); } - else if (_targetTypeConverter is not null && value is not null) + else if (_targetTypeConverter 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); + value = ConvertFrom(_targetTypeConverter, value); } } // 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); + value = ConvertFallback(FallbackValue, nameof(FallbackValue)); + + // Update the stored value. + var oldValue = _value; + + if (value is null) + _value = NullReference; + else + _value = new(value); + + hasValueChanged = !Equals(oldValue, _value); + hasErrorChanged = _error is not null || (_error is null && hadError); + } + + private void PublishValue(bool hasValueChanged, bool hasErrorChanged) + { + if (!hasValueChanged && !hasErrorChanged) + return; + + if (_sink is not null && _produceValue) + { + if (Dispatcher.UIThread.CheckAccess()) + { + _sink.OnChanged(this, hasValueChanged, hasErrorChanged); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var sink = _sink; + var v = hasValueChanged; + var e = hasErrorChanged; + Dispatcher.UIThread.Post(() => sink.OnChanged(this, v, e)); + } + } + } - // Store the value and publish the notification/value to the observer. - _value = value is null ? NullReference : new(value); - _observer.OnNext(notification ?? value); + private void UpdateAndPublishValue(object? value, Exception? dataValidationError) + { + UpdateValue( + value, + dataValidationError, + out var hasValueChanged, + out var hasErrorChanged); + + if (hasValueChanged || hasErrorChanged) + PublishValue(hasValueChanged, hasErrorChanged); } private void WriteTargetValueToSource() @@ -536,7 +720,7 @@ internal class BindingExpression : IObservable, private void OnSourceChanged(object? source) { if (_nodes.Count > 0) - _nodes[0].SetSource(source); + _nodes[0].SetSource(source, null); } private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) @@ -566,8 +750,8 @@ internal class BindingExpression : IObservable, if (ShouldLogError(out var target)) Log(target, $"{message}: {e.Message}", LogEventLevel.Warning); - var ex = new InvalidCastException(message + '.', e); - return new BindingNotification(ex, BindingErrorType.Error); + _error = new(new InvalidCastException(message + '.', e), BindingValueType.BindingError); + return AvaloniaProperty.UnsetValue; } } @@ -585,7 +769,7 @@ internal class BindingExpression : IObservable, return AvaloniaProperty.UnsetValue; } - private object? ConvertFrom(TargetTypeConverter? converter, object value) + private object? ConvertFrom(TargetTypeConverter? converter, object? value) { if (converter is null || _targetProperty is null) return value; @@ -602,7 +786,8 @@ internal class BindingExpression : IObservable, if (ShouldLogError(out var target)) Log(target, message, LogEventLevel.Warning); - return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error); + _error = new(new InvalidCastException(message), BindingValueType.BindingError); + return AvaloniaProperty.UnsetValue; } private static bool IdentityEquals(object? a, object? b, Type type) @@ -613,23 +798,16 @@ internal class BindingExpression : IObservable, return ReferenceEquals(a, b); } - private static object? UpdateAndUnwrap(object? value, ref BindingNotification? notification) + private class BindingError { - if (value is BindingNotification n) + public BindingError(Exception exception, BindingValueType errorType) { - value = n.Value; - - if (n.Error is not null) - { - if (notification is null) - notification = n; - else - notification.AddError(n.Error, n.ErrorType); - } + Exception = exception; + ErrorType = errorType; } - notification?.SetValue(value); - return value; + public Exception Exception { get; } + public BindingValueType ErrorType { get; } } private class UncommonFields @@ -640,5 +818,7 @@ internal class BindingExpression : IObservable, public object? _fallbackValue; public string? _stringFormat; public object? _targetNullValue; + public object? _defaultValue; + public bool _isDefaultValueInitialized; } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs index b5ee3925f4..96cd958f72 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs @@ -30,7 +30,7 @@ internal class ArrayIndexerNode : ExpressionNode builder.Append(']'); } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (source is Array array) SetValue(array.GetValue(_indexes)); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs index 7ca3c7f654..64155a348a 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs @@ -35,7 +35,7 @@ internal class AvaloniaPropertyAccessorNode : ExpressionNode, ISettableNode return false; } - protected override void OnSourceChanged(object? source) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { if (source is AvaloniaObject newObject) { diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs index 1df225d30f..db8a8e8080 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs @@ -22,7 +22,7 @@ internal abstract class CollectionNodeBase : ExpressionNode, UpdateValueOrSetError(sender); } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { Subscribe(source); UpdateValue(source); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs index 151988f3ac..3d0dc6eb58 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs @@ -4,7 +4,7 @@ namespace Avalonia.Data.Core.ExpressionNodes; internal class DataContextNode : DataContextNodeBase { - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (source is IDataContextProvider && source is AvaloniaObject ao) { diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs index 80607c2803..88f3bfaa57 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using System.Text; @@ -86,7 +87,7 @@ internal abstract class ExpressionNode /// public void Reset() { - SetSource(null); + SetSource(null, null); _source = _value = null; } @@ -116,7 +117,10 @@ internal abstract class ExpressionNode /// in which case the source will be considered /// to be null. /// - public void SetSource(object? source) + /// + /// Any data validation error reported by the previous expression node. + /// + public void SetSource(object? source, Exception? dataValidationError) { var oldSource = Source; @@ -137,7 +141,7 @@ internal abstract class ExpressionNode } else if (source != oldSource) { - try { OnSourceChanged(source); } + try { OnSourceChanged(source, dataValidationError); } catch (Exception e) { SetError(e); } } } @@ -147,6 +151,17 @@ internal abstract class ExpressionNode /// protected void ClearValue() => SetValue(AvaloniaProperty.UnsetValue); + /// + /// Notifies the of a data validation error. + /// + /// The error. + protected void SetDataValidationError(Exception error) + { + if (error is TargetInvocationException tie) + error = tie.InnerException!; + Owner?.OnDataValidationError(error); + } + /// /// Sets the current value to and notifies the /// of the error. @@ -167,6 +182,8 @@ internal abstract class ExpressionNode { if (e is TargetInvocationException tie) e = tie.InnerException!; + if (e is AggregateException ae && ae.InnerExceptions.Count == 1) + e = e.InnerException!; SetError(e.Message); } @@ -174,22 +191,58 @@ internal abstract class ExpressionNode /// Sets the current , notifying the if the value /// has changed. /// - /// The new value. - protected void SetValue(object? value) + /// + /// The new value. May be a . + /// + protected void SetValue(object? valueOrNotification) { + if (valueOrNotification is BindingNotification notification) + { + if (notification.ErrorType == BindingErrorType.Error) + SetError(notification.Error!); + else if (notification.ErrorType == BindingErrorType.DataValidationError) + SetValue(notification.Value, notification.Error); + else + SetValue(notification.Value, null); + } + else + { + SetValue(valueOrNotification, null); + } + } + + /// + /// Sets the current , notifying the if the value + /// has changed. + /// + /// + /// The new value. May not be a . + /// + /// + /// The data validation error associated with the new value, if any. + /// + protected void SetValue(object? value, Exception? dataValidationError = null) + { + Debug.Assert(value is not BindingNotification); + + if (Owner is null) + return; + // We raise a change notification if: // // - This is the initial value (_value is null) - // - The value is a binding notification + // - There is a data validation error + // - There is no data validation error, but the owner has one // - The old value has been GC'd - in this case we don't know if the new value is different // - The new value is different to the old value if (_value is null || - value is BindingNotification || + dataValidationError is not null || + (dataValidationError is null && Owner.HasDataValidationError) || _value.TryGetTarget(out var oldValue) == false || !Equals(oldValue, value)) { _value = value is null ? BindingExpression.NullReference : new(value); - Owner?.OnNodeValueChanged(Index, value); + Owner.OnNodeValueChanged(Index, value, dataValidationError); } } @@ -198,7 +251,10 @@ internal abstract class ExpressionNode /// . /// /// The new source. - protected abstract void OnSourceChanged(object source); + /// + /// Any data validation error reported by the previous expression node. + /// + protected abstract void OnSourceChanged(object source, Exception? dataValidationError); /// /// When implemented in a derived class, unsubscribes from the previous source. diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs index ebe8b56c06..0bd0b52df6 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs @@ -21,7 +21,7 @@ internal class FuncTransformNode : ExpressionNode // We don't have enough information to add anything here. } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { SetValue(_transform(source)); } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs index 8fcbb79087..a280fbbc4a 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs @@ -56,7 +56,7 @@ internal class LogicalAncestorElementNode : SourceNode return target is ILogical logical && logical.IsAttachedToLogicalTree; } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (source is ILogical logical) { diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs index 2b1df46595..62e4ec0ac2 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs @@ -28,13 +28,13 @@ internal class LogicalNotNode : ExpressionNode, ISettableNode return false; } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { var v = BindingNotification.ExtractValue(source); if (TryConvert(v, out var value)) { - SetValue(BindingNotification.UpdateValue(source, !value)); + SetValue(BindingNotification.UpdateValue(source, !value), dataValidationError); } else SetError($"Unable to convert '{source}' to bool."); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs index ea159fb8e7..c54061af72 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs @@ -38,7 +38,7 @@ internal class MethodCommandNode : ExpressionNode builder.Append("()"); } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (source is INotifyPropertyChanged newInpc) newInpc.PropertyChanged += OnPropertyChanged; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/NamedElementNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/NamedElementNode.cs index 71066d5888..0c96cc2ea5 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/NamedElementNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/NamedElementNode.cs @@ -30,7 +30,7 @@ internal class NamedElementNode : SourceNode return target is not ILogical logical || logical.IsAttachedToLogicalTree; } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (_nameScope.TryGetTarget(out var scope)) _subscription = NameScopeLocator.Track(scope, _name).Subscribe(SetValue); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/ParentDataContextNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/ParentDataContextNode.cs index 0b6b308ffb..1aa03e1dba 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/ParentDataContextNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/ParentDataContextNode.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Data.Core.ExpressionNodes; +using System; + +namespace Avalonia.Data.Core.ExpressionNodes; /// /// A node in an which selects the value of the visual @@ -9,7 +11,7 @@ internal class ParentDataContextNode : DataContextNodeBase private static readonly AvaloniaObject s_unset = new(); private AvaloniaObject? _parent = s_unset; - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (source is AvaloniaObject newElement) newElement.PropertyChanged += OnPropertyChanged; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/PropertyAccessorNode.cs index a6c41f5240..3dad49a2cf 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/PropertyAccessorNode.cs @@ -45,7 +45,7 @@ internal class PropertyAccessorNode : ExpressionNode, IPropertyAccessorNode, ISe return false; } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (_plugin.Start(new(source), PropertyName) is { } accessor) { diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs index 3fae393d67..8fc2b6d878 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs @@ -42,7 +42,7 @@ internal class DynamicPluginPropertyAccessorNode : ExpressionNode, IPropertyAcce return _accessor?.SetValue(value, BindingPriority.LocalValue) ?? false; } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { var reference = new WeakReference(source); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs index 304b25b6ae..b609e296ab 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs @@ -16,7 +16,7 @@ internal class DynamicPluginStreamNode : ExpressionNode builder.Append('^'); } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { var reference = new WeakReference(source); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs index 0d33434351..2d5eb58fe8 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs @@ -52,7 +52,7 @@ internal class ReflectionIndexerNode : CollectionNodeBase, ISettableNode return true; } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { _indexes = null; @@ -67,7 +67,7 @@ internal class ReflectionIndexerNode : CollectionNodeBase, ISettableNode } _indexes = ConvertIndexes(parameters, Arguments); - base.OnSourceChanged(source); + base.OnSourceChanged(source, dataValidationError); } else { diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs index 5ed89cd139..83e930b2bd 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs @@ -19,7 +19,7 @@ internal class ReflectionTypeCastNode : ExpressionNode builder.Append(')'); } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (_targetType.IsInstanceOfType(source)) SetValue(source); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs index dd379234d4..2e3b78f7f3 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs @@ -23,7 +23,7 @@ internal class StreamNode : ExpressionNode, IObserver void IObserver.OnError(Exception error) { } void IObserver.OnNext(object? value) => SetValue(value); - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (_plugin.Start(new(source)) is { } accessor) { diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs index 34d2f78cac..15705157f3 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs @@ -22,7 +22,7 @@ internal class TemplatedParentNode : SourceNode throw new InvalidOperationException("Cannot find a StyledElement to get a TemplatedParent."); } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (source is StyledElement newElement) { diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs index 6bc5f79210..15f1bf7375 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs @@ -56,7 +56,7 @@ internal class VisualAncestorElementNode : SourceNode return target is Visual visual && visual.IsAttachedToVisualTree; } - protected override void OnSourceChanged(object source) + protected override void OnSourceChanged(object source, Exception? dataValidationError) { if (source is Visual visual) { diff --git a/src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs b/src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs new file mode 100644 index 0000000000..f37673769b --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Data.Core; + +internal interface IBindingExpressionSink +{ + /// + /// Called when a 's value or error state + /// changes. + /// + /// The binding expression. + void OnChanged( + BindingExpression instance, + bool hasValueChanged, + bool hasErrorChanged); + + /// + /// Called when a completes. + /// + /// The binding expression. + void OnCompleted(BindingExpression instance); +} diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index dd02553eaf..ef1b67615f 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using Avalonia.Data; namespace Avalonia @@ -142,5 +141,11 @@ namespace Avalonia Setter((TOwner)instance, (TValue)value!); } + + object? IDirectPropertyAccessor.GetUnsetValue(Type type) + { + var metadata = GetMetadata(type); + return metadata.UnsetValue; + } } } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 4dddc12794..d1b7c1aaea 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.PropertyStore; namespace Avalonia @@ -157,6 +158,12 @@ namespace Avalonia return null; } + internal override void RouteSetDirectValueUnchecked(AvaloniaObject o, object? value) + { + var bindingValue = BindingValue.FromUntypedStrict(value); + o.SetDirectValueUnchecked(this, bindingValue); + } + internal override void RouteSetCurrentValue(AvaloniaObject o, object? value) { RouteSetValue(o, value, BindingPriority.LocalValue); @@ -175,5 +182,10 @@ namespace Avalonia { return o.Bind(this, source); } + + internal override IDisposable RouteBind(AvaloniaObject o, BindingExpression source, BindingPriority priority) + { + return o.GetValueStore().AddBinding(this, source); + } } } diff --git a/src/Avalonia.Base/IDirectPropertyAccessor.cs b/src/Avalonia.Base/IDirectPropertyAccessor.cs index 016ca9a6fb..f92ca1fbf8 100644 --- a/src/Avalonia.Base/IDirectPropertyAccessor.cs +++ b/src/Avalonia.Base/IDirectPropertyAccessor.cs @@ -32,5 +32,11 @@ namespace Avalonia /// The instance. /// The value. void SetValue(AvaloniaObject instance, object? value); + + /// + /// Gets the unset value of the property for the specified type. + /// + /// The type. + object? GetUnsetValue(Type type); } } diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index a00a51e694..da7bec836e 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -127,6 +127,18 @@ namespace Avalonia.PropertyStore IValueEntry value, BindingPriority priority); + /// + /// Sets the value and base value for a LocalValue priority, raising + /// where necessary. + /// + /// The associated value store. + /// The property being changed. + /// The new value of the property. + public abstract void SetLocalValueAndRaise( + ValueStore owner, + AvaloniaProperty property, + object? value); + /// /// Raises in response to an inherited value /// change. diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 832a56ad13..3216273b89 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -66,6 +66,14 @@ namespace Avalonia.PropertyStore } } + public override void SetLocalValueAndRaise( + ValueStore owner, + AvaloniaProperty property, + object? value) + { + SetLocalValueAndRaise(owner, (StyledProperty)property, (T)value!); + } + public void SetLocalValueAndRaise( ValueStore owner, StyledProperty property, @@ -138,6 +146,10 @@ namespace Avalonia.PropertyStore public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) { + var clearDataValidation = ValueEntry?.GetDataValidationState(out _, out _) ?? + BaseValueEntry?.GetDataValidationState(out _, out _) ?? + false; + ValueEntry?.Unsubscribe(); BaseValueEntry?.Unsubscribe(); @@ -163,12 +175,8 @@ namespace Avalonia.PropertyStore owner.OnInheritedEffectiveValueDisposed(p, Value, newValue); } - if (ValueEntry?.GetDataValidationState(out _, out _) ?? - BaseValueEntry?.GetDataValidationState(out _, out _) ?? - false) - { + if (clearDataValidation) owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null); - } } protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property) diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs index 222d857aa3..7c14c85a2d 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Data; +using Avalonia.Data.Core; namespace Avalonia.PropertyStore { @@ -14,6 +15,14 @@ namespace Avalonia.PropertyStore { } + public IValueEntry AddBinding( + StyledProperty property, + BindingExpression source) + { + Add(source); + return source; + } + public TypedBindingEntry AddBinding( StyledProperty property, IObservable> source) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 2047f4d2d0..cf3636df77 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -4,13 +4,15 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.Diagnostics; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.Utilities; namespace Avalonia.PropertyStore { - internal class ValueStore + internal class ValueStore : IBindingExpressionSink { private readonly List _frames = new(); private Dictionary? _localValueBindings; @@ -41,6 +43,33 @@ namespace Avalonia.PropertyStore ReevaluateEffectiveValues(); } + public IDisposable AddBinding( + StyledProperty property, + BindingExpression source, + BindingPriority priority) + { + if (priority == BindingPriority.LocalValue) + { + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = source; + source.Start(this, property, priority); + return source; + } + else + { + var effective = GetEffectiveValue(property); + var frame = GetOrCreateImmediateValueFrame(property, priority, out _); + + frame.AddBinding(property, source); + + if (effective is null || priority <= effective.Priority) + source.Start(this, property, priority); + + return source; + } + } + public IDisposable AddBinding( StyledProperty property, IObservable> source, @@ -125,6 +154,15 @@ namespace Avalonia.PropertyStore } } + public IDisposable AddBinding(DirectPropertyBase property, BindingExpression source) + { + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = source; + source.Start(this, property, BindingPriority.LocalValue); + return source; + } + public IDisposable AddBinding(DirectPropertyBase property, IObservable> source) { var observer = new DirectBindingObserver(this, property); @@ -212,6 +250,20 @@ namespace Avalonia.PropertyStore } } + public void SetLocalValue(AvaloniaProperty property, object? value) + { + if (TryGetEffectiveValue(property, out var existing)) + { + existing.SetLocalValueAndRaise(this, property, value); + } + else + { + var effectiveValue = property.CreateEffectiveValue(Owner); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetLocalValueAndRaise(this, property, value); + } + } + public void SetLocalValue(StyledProperty property, T value) { if (TryGetEffectiveValue(property, out var existing)) @@ -405,6 +457,7 @@ namespace Avalonia.PropertyStore /// /// The binding entry. /// The priority of binding which produced a new value. + [Obsolete("TODO: Remove?")] public void OnBindingValueChanged( IValueEntry entry, BindingPriority priority) @@ -528,6 +581,7 @@ namespace Avalonia.PropertyStore /// /// The previously bound property. /// The observer. + [Obsolete("TODO: Remove?")] public void OnLocalValueBindingCompleted(AvaloniaProperty property, IDisposable observer) { if (_localValueBindings is not null && @@ -689,6 +743,87 @@ namespace Avalonia.PropertyStore overridden); } + void IBindingExpressionSink.OnChanged( + BindingExpression instance, + bool hasValueChanged, + bool hasErrorChanged) + { + Dispatcher.UIThread.VerifyAccess(); + Debug.Assert(instance.TargetProperty is not null); + + var property = instance.TargetProperty; + var value = instance.GetValueOrDefault(); + + if (property.IsDirect) + { + if (hasValueChanged) + property.RouteSetDirectValueUnchecked(Owner, value); + } + else + { + var priority = instance.Priority; + + if (hasValueChanged) + { + if (priority == BindingPriority.LocalValue) + { + if (value != AvaloniaProperty.UnsetValue) + SetLocalValue(property, value); + else if (property == StyledElement.DataContextProperty) + SetLocalValue(property, null); + else + ClearValue(property); + } + else + { + if (TryGetEffectiveValue(property, out var existing)) + { + if (priority <= existing.BasePriority) + ReevaluateEffectiveValue(property, existing, changedValueEntry: instance); + } + else + { + AddEffectiveValueAndRaise(property, instance, priority); + } + } + } + } + + if (instance.IsDataValidationEnabled) + { + instance.GetDataValidationState(out var state, out var error); + Owner.OnUpdateDataValidation(property, state, error); + } + } + + /// + /// Called by a binding expression when the binding produces completes. + /// + /// The binding expression. + void IBindingExpressionSink.OnCompleted(BindingExpression instance) + { + Dispatcher.UIThread.VerifyAccess(); + Debug.Assert(instance.TargetProperty is not null); + + var property = instance.TargetProperty; + + if (instance.IsDataValidationEnabled) + Owner.OnUpdateDataValidation(property, BindingValueType.UnsetValue, null); + + if (instance.Priority == BindingPriority.LocalValue) + { + if (_localValueBindings is not null && + _localValueBindings.TryGetValue(property.Id, out var existing)) + { + if (existing == instance) + { + _localValueBindings?.Remove(property.Id); + ClearValue(property); + } + } + } + } + private int InsertFrame(ValueFrame frame) { Debug.Assert(!_frames.Contains(frame)); diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index b6bd737234..e9ee3add9e 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.PropertyStore; using Avalonia.Utilities; @@ -160,10 +161,16 @@ namespace Avalonia bool IStyledPropertyAccessor.ValidateValue(object? value) { - if (value is null && !typeof(TValue).IsValueType) - return ValidateValue?.Invoke(default!) ?? true; - if (value is TValue typed) + if (value is null) + { + if (!typeof(TValue).IsValueType || Nullable.GetUnderlyingType(typeof(TValue)) != null) + return ValidateValue?.Invoke(default!) ?? true; + } + else if (value is TValue typed) + { return ValidateValue?.Invoke(typed) ?? true; + } + return false; } @@ -221,6 +228,11 @@ namespace Avalonia return target.Bind(this, source, priority); } + internal override IDisposable RouteBind(AvaloniaObject o, BindingExpression source, BindingPriority priority) + { + return o.GetValueStore().AddBinding(this, source, priority); + } + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)] private bool ShouldSetValue(AvaloniaObject target, object? value, [NotNullWhen(true)] out TValue? converted) { diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index 1ac26c79ec..eafaab7f20 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Animation; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.Metadata; using Avalonia.PropertyStore; @@ -117,7 +118,13 @@ namespace Avalonia.Styling if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) { - return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source); + if (i.Source is BindingExpression expression) + { + expression.Initialize(target.GetValueStore(), Property, instance.Priority); + return expression; + } + else + return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source); } throw new NotSupportedException(); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index d974351b1e..e483a8646c 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.PropertyStore; using Xunit; @@ -174,6 +175,11 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } + internal override IDisposable RouteBind(AvaloniaObject o, BindingExpression source, BindingPriority priority) + { + throw new NotImplementedException(); + } + internal override void RouteClearValue(AvaloniaObject o) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index 0cfc4e33ac..67cb97044f 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -105,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = BindingExpression.Create( - data, + data, o => o.StringValue, fallbackValue: 42, targetProperty: TargetTypeInt); @@ -126,7 +126,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = BindingExpression.Create( - data, + data, o => o.StringValue, enableDataValidation: true, fallbackValue: 42, @@ -253,7 +253,7 @@ namespace Avalonia.Base.UnitTests.Data.Core new BindingNotification($"{3.4}"), new BindingNotification( new InvalidCastException("Could not convert 'bar' (System.String) to System.Double."), - BindingErrorType.DataValidationError) + BindingErrorType.DataValidationError, $"{3.4}") }, result); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs index 8d14d94dbe..b8c0ffeb2b 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs @@ -92,9 +92,6 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { new BindingNotification(0), - - // Value is notified twice as ErrorsChanged is always called by IndeiTest. - new BindingNotification(5), new BindingNotification(5), // Value is first signalled without an error as validation hasn't been updated. @@ -104,7 +101,8 @@ namespace Avalonia.Base.UnitTests.Data.Core // Exception is thrown by trying to set value to "foo". new BindingNotification( new InvalidCastException("Could not convert 'foo' (System.String) to System.Int32."), - BindingErrorType.DataValidationError), + BindingErrorType.DataValidationError, + -5), // Value is set then validation is updated. new BindingNotification(new DataValidationException("Must be positive"), BindingErrorType.DataValidationError, 5), diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs index 2f6aa49c43..a98ab7d327 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs @@ -87,7 +87,7 @@ namespace Avalonia.Base.UnitTests.Data.Core new[] { new BindingNotification( - new AggregateException(new NotSupportedException()), + new BindingChainException("Specified method is not supported.", "Foo^", "^"), BindingErrorType.Error) }, result); @@ -111,7 +111,7 @@ namespace Avalonia.Base.UnitTests.Data.Core new[] { new BindingNotification( - new AggregateException(new NotSupportedException()), + new BindingChainException("Specified method is not supported.", "Foo^", "^"), BindingErrorType.Error) }, result);