diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7a0ae7d1d9..e54dc13a83 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -611,7 +611,7 @@ namespace Avalonia internal IDisposable Bind( AvaloniaProperty property, - BindingExpression expression, + UntypedBindingExpressionBase expression, BindingPriority priority) { return property.RouteBind(this, expression, priority); diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 48993ee402..934bb29cde 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -569,7 +569,7 @@ namespace Avalonia /// The priority. internal abstract IDisposable RouteBind( AvaloniaObject o, - BindingExpression source, + UntypedBindingExpressionBase source, BindingPriority priority); /// diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index a58059d891..ed017e29a1 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace Avalonia.Data { @@ -288,4 +289,17 @@ namespace Avalonia.Data a?.Message == b?.Message; } } + + internal static class BindingErrorTypeExtensions + { + public static BindingValueType ToBindingValueType(this BindingErrorType type) + { + return type switch + { + BindingErrorType.Error => BindingValueType.BindingError, + BindingErrorType.DataValidationError => BindingValueType.DataValidationError, + _ => BindingValueType.Value, + }; + } + } } diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 1dd845cded..be7218950f 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -24,7 +24,7 @@ namespace Avalonia.Data _ = property ?? throw new ArgumentNullException(nameof(property)); _ = binding ?? throw new ArgumentNullException(nameof(binding)); - if (binding.Source is BindingExpression expression) + if (binding.Expression is { } expression) { return target.Bind(property, expression, BindingPriority.LocalValue); } diff --git a/src/Avalonia.Base/Data/Core/BindingError.cs b/src/Avalonia.Base/Data/Core/BindingError.cs new file mode 100644 index 0000000000..b1cc9b9caf --- /dev/null +++ b/src/Avalonia.Base/Data/Core/BindingError.cs @@ -0,0 +1,18 @@ +using System; +using System.Diagnostics; + +namespace Avalonia.Data.Core; + +internal class BindingError +{ + public BindingError(Exception exception, BindingErrorType errorType) + { + Debug.Assert(errorType != BindingErrorType.None); + + Exception = exception; + ErrorType = errorType; + } + + public Exception Exception { get; } + public BindingErrorType ErrorType { get; } +} diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.Observable.cs b/src/Avalonia.Base/Data/Core/BindingExpression.Observable.cs deleted file mode 100644 index 121e455fb8..0000000000 --- a/src/Avalonia.Base/Data/Core/BindingExpression.Observable.cs +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index 40cbf88b1b..0000000000 --- a/src/Avalonia.Base/Data/Core/BindingExpression.ValueEntry.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 d18f8fec15..c7e9b79cf9 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -10,6 +10,7 @@ using Avalonia.Data.Core.ExpressionNodes; using Avalonia.Data.Core.Parsers; using Avalonia.Logging; using Avalonia.Threading; +using Avalonia.Utilities; namespace Avalonia.Data.Core; @@ -20,22 +21,13 @@ namespace Avalonia.Data.Core; /// A represents a untyped binding which has been /// instantiated on an object. /// -internal partial class BindingExpression : IDescription, IDisposable +internal partial class BindingExpression : UntypedBindingExpressionBase, 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 TargetTypeConverter? _targetTypeConverter; - private bool _isRunning; - private BindingPriority _priority; - private bool _produceValue; - private IBindingExpressionSink? _sink; - private AvaloniaProperty? _targetProperty; - private WeakReference? _value; - private BindingError? _error; - private UncommonFields? _uncommon; + private readonly UncommonFields? _uncommon; /// /// Initializes a new instance of the class. @@ -53,9 +45,7 @@ internal partial class BindingExpression : IDescription, IDisposable /// /// 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. /// @@ -69,28 +59,20 @@ internal partial class BindingExpression : IDescription, IDisposable bool enableDataValidation = false, BindingMode mode = BindingMode.OneWay, string? stringFormat = null, - AvaloniaObject? target = null, object? targetNullValue = null, - AvaloniaProperty? targetProperty = null, TargetTypeConverter? targetTypeConverter = null) + : base(enableDataValidation) { 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; - IsDataValidationEnabled = enableDataValidation; if (converter is not null || converterCulture is not null || @@ -139,83 +121,15 @@ internal partial class BindingExpression : IDescription, IDisposable } } - 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. /// @@ -223,7 +137,7 @@ internal partial class BindingExpression : IDescription, IDisposable /// /// True if the value could be written to the binding source; otherwise false. /// - public bool SetValue(object? value) + public override bool WriteValueToSource(object? value) { if (_nodes.Count == 0 || LeafNode is not ISettableNode setter || setter.ValueType is not { } type) return false; @@ -261,7 +175,7 @@ internal partial class BindingExpression : IDescription, IDisposable } // Don't set the value if it's unchanged. - if (LeafNode.IsValueAlive && IdentityEquals(LeafNode.Value, value, type)) + if (LeafNode.IsValueAlive && TypeUtilities.IdentityEquals(LeafNode.Value, value, type)) return true; try @@ -274,61 +188,6 @@ internal partial class BindingExpression : IDescription, IDisposable } } - /// - /// 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. /// @@ -342,9 +201,7 @@ internal partial class BindingExpression : IDescription, IDisposable /// 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. /// Whether to allow reflection for target type conversion. [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] internal static BindingExpression Create( @@ -356,9 +213,7 @@ internal partial class BindingExpression : IDescription, IDisposable bool enableDataValidation = false, Optional fallbackValue = default, BindingMode mode = BindingMode.OneWay, - AvaloniaObject? target = null, object? targetNullValue = null, - AvaloniaProperty? targetProperty = null, bool allowReflection = true) where TIn : class? { @@ -374,9 +229,7 @@ internal partial class BindingExpression : IDescription, IDisposable converterParameter: converterParameter, enableDataValidation: enableDataValidation, mode: mode, - target: target, targetNullValue: targetNullValue, - targetProperty: targetProperty, targetTypeConverter: allowReflection ? TargetTypeConverter.GetReflectionConverter() : TargetTypeConverter.GetDefaultConverter()); @@ -400,7 +253,12 @@ internal partial class BindingExpression : IDescription, IDisposable // The leaf node has changed. If the binding mode is not OneWayToSource, publish the // value to the target. if (_mode != BindingMode.OneWayToSource) - UpdateAndPublishValue(value, dataValidationError); + { + var error = dataValidationError is not null ? + new BindingError(dataValidationError, BindingErrorType.DataValidationError) : + null; + ConvertAndPublishValue(value, error); + } // If the binding mode is OneTime, then stop the binding. if (_mode == BindingMode.OneTime) @@ -448,21 +306,17 @@ internal partial class BindingExpression : IDescription, IDisposable if (ShouldLogError(out var target)) Log(target, error, errorPoint); - // Clear the current value. - UpdateValue(AvaloniaProperty.UnsetValue, null, out var hasValueChanged, out _); - - // And store the error. - _error = new( + // Clear the current value and publish the error. + var bindingError = new BindingError( new BindingChainException(error, Description, errorPoint.ToString()), - BindingValueType.BindingError); - - PublishValue(hasValueChanged, hasErrorChanged: true); + BindingErrorType.Error); + ConvertAndPublishValue(AvaloniaProperty.UnsetValue, bindingError); } internal void OnDataValidationError(Exception error) { - _error = new(error, BindingValueType.DataValidationError); - PublishValue(hasValueChanged: false, hasErrorChanged: true); + var bindingError = new BindingError(error, BindingErrorType.DataValidationError); + PublishValue(UnchangedValue, bindingError); } private string CalculateErrorPoint(int nodeIndex) @@ -478,27 +332,6 @@ internal partial class BindingExpression : IDescription, IDisposable 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)) @@ -507,7 +340,7 @@ internal partial class BindingExpression : IDescription, IDisposable log.Log( target, "An error occurred binding {Property} to {Expression}: {Message}", - (object?)_targetProperty ?? "(unknown)", + (object?)TargetProperty ?? "(unknown)", Description, error); } @@ -520,7 +353,7 @@ internal partial class BindingExpression : IDescription, IDisposable log.Log( target, "An error occurred binding {Property} to {Expression} at {ExpressionErrorPoint}: {Message}", - (object?)_targetProperty ?? "(unknown)", + (object?)TargetProperty ?? "(unknown)", Description, errorPoint, error); @@ -528,42 +361,28 @@ internal partial class BindingExpression : IDescription, IDisposable private bool ShouldLogError([NotNullWhen(true)] out AvaloniaObject? target) { - if (!_target.TryGetTarget(out target)) + if (!TryGetTarget(out target)) return false; if (_nodes.Count > 0 && _nodes[0] is SourceNode sourceNode) return sourceNode.ShouldLogErrors(target); return true; } - private void Start(bool produceValue) + protected override void StartCore() { - 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, null); - } else - { - _value = new(source); - _error = null; - PublishValue(hasValueChanged: true, hasErrorChanged: false); - } + PublishValue(source); if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && - _target.TryGetTarget(out var target) && - _targetProperty is not null) + TryGetTarget(out var target) && + TargetProperty is not null) { if (_mode is BindingMode.OneWayToSource) - SetValue(target.GetValue(_targetProperty)); + PublishValue(target.GetValue(TargetProperty)); target.PropertyChanged += OnTargetPropertyChanged; } @@ -572,58 +391,31 @@ internal partial class BindingExpression : IDescription, IDisposable { OnNodeError(-1, "Binding Source is null."); } - - _produceValue = true; } - private void Stop() + protected override void StopCore() { foreach (var node in _nodes) node.Reset(); if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && - _target.TryGetTarget(out var target)) + TryGetTarget(out var target)) { target.PropertyChanged -= OnTargetPropertyChanged; } - - _isRunning = false; - _value = null; } - private void UpdateValue( - object? value, - Exception? dataValidationError, - out bool hasValueChanged, - out bool hasErrorChanged) + private void ConvertAndPublishValue(object? value, BindingError? error) { 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 = Convert(converter, ConverterParameter, value, TargetType); + value = Convert(converter, ConverterParameter, value, TargetType, ref error); // 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. @@ -647,7 +439,7 @@ internal partial class BindingExpression : IDescription, IDisposable else if (_targetTypeConverter is not null) { // Otherwise, if we have a target type converter, convert the value to the target type. - value = ConvertFrom(_targetTypeConverter, value); + value = ConvertFrom(_targetTypeConverter, value, ref error); } } @@ -656,78 +448,30 @@ internal partial class BindingExpression : IDescription, IDisposable if (value == AvaloniaProperty.UnsetValue && FallbackValue != AvaloniaProperty.UnsetValue) 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)); - } - } - } - - private void UpdateAndPublishValue(object? value, Exception? dataValidationError) - { - UpdateValue( - value, - dataValidationError, - out var hasValueChanged, - out var hasErrorChanged); - - if (hasValueChanged || hasErrorChanged) - PublishValue(hasValueChanged, hasErrorChanged); + // Publish the value. + PublishValue(value, error); } private void WriteTargetValueToSource() { - if (_mode != BindingMode.OneWayToSource) - return; + Debug.Assert(_mode == BindingMode.OneWayToSource); - if (_target.TryGetTarget(out var target) && - _targetProperty is not null && - target.GetValue(_targetProperty) is var value && - !Equals(value, LeafNode.Value)) + if (TryGetTarget(out var target) && + TargetProperty is not null && + target.GetValue(TargetProperty) is var value && + !TypeUtilities.IdentityEquals(value, LeafNode.Value, TargetType)) { - SetValue(value); + WriteValueToSource(value); } } - private void OnSourceChanged(object? source) - { - if (_nodes.Count > 0) - _nodes[0].SetSource(source, null); - } - private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { - if (e.Property == _targetProperty) + Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource); + + if (e.Property == TargetProperty) { - SetValue(e.NewValue); + WriteValueToSource(e.NewValue); } } @@ -735,7 +479,8 @@ internal partial class BindingExpression : IDescription, IDisposable IValueConverter converter, object? converterParameter, object? value, - Type targetType) + Type targetType, + ref BindingError? error) { try { @@ -750,7 +495,7 @@ internal partial class BindingExpression : IDescription, IDisposable if (ShouldLogError(out var target)) Log(target, $"{message}: {e.Message}", LogEventLevel.Warning); - _error = new(new InvalidCastException(message + '.', e), BindingValueType.BindingError); + error = new(new InvalidCastException(message + '.', e), BindingErrorType.Error); return AvaloniaProperty.UnsetValue; } } @@ -763,53 +508,31 @@ internal partial class BindingExpression : IDescription, IDisposable if (_targetTypeConverter.TryConvert(fallback, TargetType, ConverterCulture, out var result)) return result; - if (_target.TryGetTarget(out var target)) + if (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) + private object? ConvertFrom(TargetTypeConverter? converter, object? value, ref BindingError? error) { - if (converter is null || _targetProperty is null) + if (converter is null) return value; - var targetType = _targetProperty.PropertyType; - - if (converter.TryConvert(value, targetType, ConverterCulture, out var result)) + if (converter.TryConvert(value, TargetType, ConverterCulture, 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}'."; + var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{TargetType}'."; if (ShouldLogError(out var target)) Log(target, message, LogEventLevel.Warning); - _error = new(new InvalidCastException(message), BindingValueType.BindingError); + error = new(new InvalidCastException(message), BindingErrorType.Error); return AvaloniaProperty.UnsetValue; } - private static bool IdentityEquals(object? a, object? b, Type type) - { - if (type.IsValueType || type == typeof(string)) - return Equals(a, b); - else - return ReferenceEquals(a, b); - } - - private class BindingError - { - public BindingError(Exception exception, BindingValueType errorType) - { - Exception = exception; - ErrorType = errorType; - } - - public Exception Exception { get; } - public BindingValueType ErrorType { get; } - } - private class UncommonFields { public IValueConverter? _converter; @@ -818,7 +541,5 @@ internal partial class BindingExpression : IDescription, IDisposable public object? _fallbackValue; public string? _stringFormat; public object? _targetNullValue; - public object? _defaultValue; - public bool _isDefaultValueInitialized; } } diff --git a/src/Avalonia.Base/Data/Core/BindingExpressionBase.cs b/src/Avalonia.Base/Data/Core/BindingExpressionBase.cs new file mode 100644 index 0000000000..ee6f524a0a --- /dev/null +++ b/src/Avalonia.Base/Data/Core/BindingExpressionBase.cs @@ -0,0 +1,10 @@ +using Avalonia.Styling; + +namespace Avalonia.Data.Core; + +public abstract class BindingExpressionBase : ISetterInstance +{ + private protected BindingExpressionBase() + { + } +} diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs index 88f3bfaa57..fc86213373 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs @@ -59,7 +59,7 @@ internal abstract class ExpressionNode { get { - return _value == BindingExpression.NullReference || + return _value == UntypedBindingExpressionBase._nullReference || _value?.TryGetTarget(out _) == true; } } @@ -137,7 +137,7 @@ internal abstract class ExpressionNode // If the source is null then the value is null. We explicitly do not want to call // OnSourceChanged as we don't want to raise errors for subsequent nodes in the // binding change. - _value = BindingExpression.NullReference; + _value = BindingExpression._nullReference; } else if (source != oldSource) { @@ -199,11 +199,20 @@ internal abstract class ExpressionNode if (valueOrNotification is BindingNotification notification) { if (notification.ErrorType == BindingErrorType.Error) + { SetError(notification.Error!); + } else if (notification.ErrorType == BindingErrorType.DataValidationError) - SetValue(notification.Value, notification.Error); + { + if (notification.HasValue) + SetValue(notification.Value, notification.Error); + else + SetDataValidationError(notification.Error!); + } else + { SetValue(notification.Value, null); + } } else { @@ -237,11 +246,11 @@ internal abstract class ExpressionNode // - The new value is different to the old value if (_value is null || dataValidationError is not null || - (dataValidationError is null && Owner.HasDataValidationError) || + (dataValidationError is null && Owner.ErrorType == BindingErrorType.DataValidationError) || _value.TryGetTarget(out var oldValue) == false || !Equals(oldValue, value)) { - _value = value is null ? BindingExpression.NullReference : new(value); + _value = value is null ? BindingExpression._nullReference : new(value); Owner.OnNodeValueChanged(Index, value, dataValidationError); } } diff --git a/src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs b/src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs index f37673769b..a068536e1c 100644 --- a/src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs +++ b/src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs @@ -3,18 +3,32 @@ internal interface IBindingExpressionSink { /// - /// Called when a 's value or error state + /// Called when an 's value or error state /// changes. /// /// The binding expression. + /// + /// Indicates whether represents a new value produced by the binding. + /// + /// + /// Indicates whether represents a new error produced by the binding. + /// + /// + /// The new binding value; if is true. + /// + /// + /// The new binding error; if is true. + /// void OnChanged( - BindingExpression instance, + UntypedBindingExpressionBase instance, bool hasValueChanged, - bool hasErrorChanged); + bool hasErrorChanged, + object? value, + BindingError? error); /// - /// Called when a completes. + /// Called when an completes. /// /// The binding expression. - void OnCompleted(BindingExpression instance); + void OnCompleted(UntypedBindingExpressionBase instance); } diff --git a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs new file mode 100644 index 0000000000..352f12064c --- /dev/null +++ b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs @@ -0,0 +1,455 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.PropertyStore; +using Avalonia.Reactive; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Data.Core; + +/// +/// Base class for binding expressions which produce untyped values. +/// +internal abstract class UntypedBindingExpressionBase : BindingExpressionBase, IDisposable, IValueEntry +{ + protected static readonly object UnchangedValue = new(); + internal static readonly WeakReference _nullReference = new(null); + private readonly bool _isDataValidationEnabled; + private object? _defaultValue; + private BindingError? _error; + private bool _isDefaultValueInitialized; + private bool _isRunning; + private bool _produceValue; + private IBindingExpressionSink? _sink; + private WeakReference? _target; + private WeakReference? _value; + + /// + /// Initializes a new instance of the class. + /// + /// Whether data validation is enabled. + public UntypedBindingExpressionBase(bool isDataValidationEnabled) + { + _isDataValidationEnabled = isDataValidationEnabled; + } + + /// + /// Gets the current error state of the binding expression. + /// + public BindingErrorType ErrorType => _error?.ErrorType ?? BindingErrorType.None; + + /// + /// Gets a value indicating whether data validation is enabled for the binding expression. + /// + public bool IsDataValidationEnabled => _isDataValidationEnabled; + + /// + /// Gets a value indicating whether the binding expression is currently running. + /// + public bool IsRunning => _isRunning; + + /// + /// Gets the priority of the binding expression. + /// + public BindingPriority Priority { get; private set; } + + /// + /// Gets the which the binding expression is targeting. + /// + public AvaloniaProperty? TargetProperty { get; private set; } + + /// + /// Gets the target type of the binding expression; that is, the type that values produced by + /// the expression should be converted to. + /// + public Type TargetType { get; private set; } = typeof(object); + + bool IValueEntry.HasValue + { + get + { + Start(produceValue: false); + return _value is not null; + } + } + + AvaloniaProperty IValueEntry.Property => TargetProperty ?? throw new Exception(); + + /// + /// Produces an observable which can be used to observe the value of the binding expression. + /// + /// An observable subject. + /// + /// The binding expression is already instantiated on an AvaloniaObject. + /// + /// + /// This method is mostly here for backwards compatibility with + /// and unit testing and we may want to remove it in future. In particular its usefulness in + /// terms of unit testing is limited in that it preserves the semantics of binding expressions + /// as expected by unit tests, not necessarily the semantics that will be used when the + /// expression is used as an instantiated in a + /// . Unit tests should be migrated to not test the behaviour of + /// binding expressions through an observable, and instead test the behaviour of the binding + /// when applied to an . + /// + /// A binding expression may only act as an observable or as a binding expression targeting an + /// AvaloniaObject, not both. + /// + public IAvaloniaSubject ToObservable() + { + if (_sink is ObservableSink s) + return s; + if (_sink is not null) + throw new InvalidOperationException( + "Cannot call AsObservable on a to binding expression which is already " + + "instantiated on an AvaloniaObject."); + + var o = new ObservableSink(this); + _sink = o; + return o; + } + + /// + /// Produces an observable which can be used to observe the value of the binding expression + /// for unit testing. + /// + /// The . + /// An observable subject. + /// + /// The binding expression is already instantiated on an AvaloniaObject. + /// + /// + /// This method should be considered obsolete and new unit tests should not be written to use + /// it. For more information see . + /// + public IAvaloniaSubject ToObservable(Type targetType) + { + var o = ToObservable(); + TargetType = targetType; + return o; + } + + /// + /// Terminates the binding. + /// + public virtual void Dispose() + { + if (_sink is null) + return; + + Stop(); + + var sink = _sink; + _sink = null; + sink.OnCompleted(this); + } + + /// + /// 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. + /// + public object? GetValueOrDefault() + { + var result = GetValue(); + if (result == AvaloniaProperty.UnsetValue) + result = GetCachedDefaultValue(); + return result; + } + + /// + /// Attaches the binding expression to a subscriber with the specified subscriber but does not + /// start it. + /// + /// The subscriber. + /// The target object. + /// The target property. + /// The priority of the binding. + public void Attach( + IBindingExpressionSink sink, + AvaloniaObject target, + AvaloniaProperty targetProperty, + BindingPriority priority) + { + if (_sink is not null) + throw new InvalidOperationException("BindingExpression was already instantiated."); + + _sink = sink; + _target = new(target); + TargetProperty = targetProperty; + TargetType = targetProperty.PropertyType; + Priority = priority; + } + + /// + /// Initializes the binding expression with the specified subscriber and target property and + /// starts it. + /// + /// The subscriber. + /// The target object. + /// The target property. + /// The priority of the binding. + public void Start( + IBindingExpressionSink subscriber, + AvaloniaObject target, + AvaloniaProperty targetProperty, + BindingPriority priority) + { + Attach(subscriber, target, targetProperty, priority); + Start(produceValue: true); + } + + /// + /// When overridden in a derived class, 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 abstract bool WriteValueToSource(object? value); + + bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error) + { + if (_error is not null) + { + state = _error.ErrorType switch + { + BindingErrorType.Error => BindingValueType.BindingError, + BindingErrorType.DataValidationError => BindingValueType.DataValidationError, + _ => throw new InvalidOperationException("Invalid BindingErrorType."), + }; + error = _error.Exception; + } + else + { + state = BindingValueType.Value; + error = null; + } + + return IsDataValidationEnabled; + } + + object? IValueEntry.GetValue() + { + Start(produceValue: false); + return GetValueOrDefault(); + } + + void IValueEntry.Unsubscribe() => Stop(); + + /// + /// When overridden in a derived class, starts the binding expression. + /// + /// + /// This method should not be called directly; instead call . + /// + protected abstract void StartCore(); + + /// + /// When overridden in a derived class, stops the binding expression. + /// + /// + /// This method should not be called directly; instead call . + /// + protected abstract void StopCore(); + + /// + /// Publishes a new value and/or error state to the target. + /// + /// The new value, or . + /// The new binding or data validation error. + protected void PublishValue(object? value, BindingError? error = null) + { + // When binding to DataContext and the expression results in a binding error, the binding + // expression should produce null rather than UnsetValue in order to not propagate + // incorrect DataContexts from parent controls while things are being set up. + if (TargetProperty == StyledElement.DataContextProperty && + value == AvaloniaProperty.UnsetValue && + error?.ErrorType == BindingErrorType.Error) + { + value = null; + } + + var hasValueChanged = value != UnchangedValue && !TypeUtilities.IdentityEquals(value, GetValue(), TargetType); + var hasErrorChanged = error is not null || _error is not null; + + if (hasValueChanged) + _value = value is null ? _nullReference : new(value); + _error = error; + + if (!_produceValue || _sink is null) + return; + + if (Dispatcher.UIThread.CheckAccess()) + { + _sink.OnChanged(this, hasValueChanged, hasErrorChanged, GetValueOrDefault(), _error); + } + 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 vc = hasValueChanged; + var ec = hasErrorChanged; + var v = GetValueOrDefault(); + var e = _error; + Dispatcher.UIThread.Post(() => sink.OnChanged(this, vc, ec, v, e)); + } + } + + /// + /// Starts the binding expression by calling . + /// + /// + /// Indicates whether the binding expression should produce an initial value. + /// + protected void Start(bool produceValue) + { + if (_isRunning) + return; + + _isRunning = true; + + try + { + _produceValue = produceValue; + StartCore(); + } + finally + { + _produceValue = true; + } + } + + /// + /// Stops the binding expression by calling . + /// + protected void Stop() + { + StopCore(); + _isRunning = false; + _value = null; + } + + /// + /// Tries to retrieve the target for the binding expression. + /// + /// + /// When this method returns, contains the target object, if it is available. + /// + /// true if the target was retrieved; otherwise, false. + protected bool TryGetTarget([NotNullWhen(true)] out AvaloniaObject? target) + { + if (_target is not null) + return _target.TryGetTarget(out target); + target = null!; + return false; + } + + private object? GetCachedDefaultValue() + { + if (_isDefaultValueInitialized == true) + return _defaultValue; + + if (TargetProperty is not null && _target?.TryGetTarget(out var target) == true) + { + if (TargetProperty.IsDirect) + _defaultValue = ((IDirectPropertyAccessor)TargetProperty).GetUnsetValue(target.GetType()); + else + _defaultValue = ((IStyledPropertyAccessor)TargetProperty).GetDefaultValue(target.GetType()); + + _isDefaultValueInitialized = true; + return _defaultValue; + } + + return AvaloniaProperty.UnsetValue; + } + + private sealed class ObservableSink : LightweightObservableBase, + IBindingExpressionSink, + IAvaloniaSubject + { + private readonly UntypedBindingExpressionBase _expression; + private WeakReference? _value; + + public ObservableSink(UntypedBindingExpressionBase expression) => _expression = expression; + + void IBindingExpressionSink.OnChanged( + UntypedBindingExpressionBase instance, + bool hasValueChanged, + bool hasErrorChanged, + object? value, + BindingError? error) + { + if (instance.IsDataValidationEnabled || error is not null) + { + BindingNotification notification; + + if (error?.ErrorType == BindingErrorType.Error) + notification = new(error.Exception, BindingErrorType.Error, value); + else if (error?.ErrorType == BindingErrorType.DataValidationError) + notification = new(error.Exception, BindingErrorType.DataValidationError, value); + else + notification = new(value); + + PublishNext(notification); + } + else if (hasValueChanged) + { + PublishNext(value); + } + } + + void IBindingExpressionSink.OnCompleted(UntypedBindingExpressionBase instance) => PublishCompleted(); + + void IObserver.OnCompleted() { } + void IObserver.OnError(Exception error) { } + void IObserver.OnNext(object? value) => _expression.WriteValueToSource(value); + + protected override void Initialize() => _expression.Start(produceValue: true); + protected override void Deinitialize() => _expression.Stop(); + + protected override void Subscribed(IObserver observer, bool first) + { + if (!first && _value is not null) + { + if (_value == _nullReference) + base.PublishNext(null); + else if (_value.TryGetTarget(out var value)) + base.PublishNext(value); + } + } + + private new void PublishNext(object? value) + { + _value = (value is null) ? _nullReference : new(value); + base.PublishNext(value); + } + } +} diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index 9bfaefbafa..03fce8da63 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using Avalonia.Data.Core; using Avalonia.Reactive; using ObservableEx = Avalonia.Reactive.Observable; @@ -16,6 +17,9 @@ namespace Avalonia.Data /// public sealed class InstancedBinding { + private IObservable? _observable; + private UntypedBindingExpressionBase? _expression; + /// /// Initializes a new instance of the class. /// @@ -32,7 +36,14 @@ namespace Avalonia.Data { Mode = mode; Priority = priority; - Source = source ?? throw new ArgumentNullException(nameof(source)); + _observable = source ?? throw new ArgumentNullException(nameof(source)); + } + + internal InstancedBinding(UntypedBindingExpressionBase source, BindingMode mode, BindingPriority priority) + { + Mode = mode; + Priority = priority; + _expression = source ?? throw new ArgumentNullException(nameof(source)); } /// @@ -48,11 +59,13 @@ namespace Avalonia.Data /// /// Gets the binding source observable. /// - public IObservable Source { get; } + public IObservable Source => _observable ??= _expression!.ToObservable(); [Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)] public IObservable Observable => Source; + internal UntypedBindingExpressionBase? Expression => _expression; + /// /// Creates a new one-time binding with a fixed value. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index d1b7c1aaea..9ecbd309ef 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -183,7 +183,10 @@ namespace Avalonia return o.Bind(this, source); } - internal override IDisposable RouteBind(AvaloniaObject o, BindingExpression source, BindingPriority priority) + internal override IDisposable RouteBind( + AvaloniaObject o, + UntypedBindingExpressionBase source, + BindingPriority priority) { return o.GetValueStore().AddBinding(this, source); } diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs index 7c14c85a2d..36d5a9a39e 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs @@ -15,9 +15,7 @@ namespace Avalonia.PropertyStore { } - public IValueEntry AddBinding( - StyledProperty property, - BindingExpression source) + public IValueEntry AddBinding(UntypedBindingExpressionBase source) { Add(source); return source; diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index cf3636df77..0dcee48621 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -45,7 +45,7 @@ namespace Avalonia.PropertyStore public IDisposable AddBinding( StyledProperty property, - BindingExpression source, + UntypedBindingExpressionBase source, BindingPriority priority) { if (priority == BindingPriority.LocalValue) @@ -53,7 +53,7 @@ namespace Avalonia.PropertyStore DisposeExistingLocalValueBinding(property); _localValueBindings ??= new(); _localValueBindings[property.Id] = source; - source.Start(this, property, priority); + source.Start(this, Owner, property, priority); return source; } else @@ -61,10 +61,10 @@ namespace Avalonia.PropertyStore var effective = GetEffectiveValue(property); var frame = GetOrCreateImmediateValueFrame(property, priority, out _); - frame.AddBinding(property, source); + frame.AddBinding(source); if (effective is null || priority <= effective.Priority) - source.Start(this, property, priority); + source.Start(this, Owner, property, priority); return source; } @@ -154,12 +154,12 @@ namespace Avalonia.PropertyStore } } - public IDisposable AddBinding(DirectPropertyBase property, BindingExpression source) + public IDisposable AddBinding(DirectPropertyBase property, UntypedBindingExpressionBase source) { DisposeExistingLocalValueBinding(property); _localValueBindings ??= new(); _localValueBindings[property.Id] = source; - source.Start(this, property, BindingPriority.LocalValue); + source.Start(this, Owner, property, BindingPriority.LocalValue); return source; } @@ -744,15 +744,16 @@ namespace Avalonia.PropertyStore } void IBindingExpressionSink.OnChanged( - BindingExpression instance, + UntypedBindingExpressionBase instance, bool hasValueChanged, - bool hasErrorChanged) + bool hasErrorChanged, + object? value, + BindingError? error) { Dispatcher.UIThread.VerifyAccess(); Debug.Assert(instance.TargetProperty is not null); var property = instance.TargetProperty; - var value = instance.GetValueOrDefault(); if (property.IsDirect) { @@ -789,10 +790,10 @@ namespace Avalonia.PropertyStore } } - if (instance.IsDataValidationEnabled) + if (hasErrorChanged && instance.IsDataValidationEnabled) { - instance.GetDataValidationState(out var state, out var error); - Owner.OnUpdateDataValidation(property, state, error); + var e = error?.ErrorType.ToBindingValueType() ?? BindingValueType.Value; + Owner.OnUpdateDataValidation(property, e, error?.Exception); } } @@ -800,7 +801,7 @@ namespace Avalonia.PropertyStore /// Called by a binding expression when the binding produces completes. /// /// The binding expression. - void IBindingExpressionSink.OnCompleted(BindingExpression instance) + void IBindingExpressionSink.OnCompleted(UntypedBindingExpressionBase instance) { Dispatcher.UIThread.VerifyAccess(); Debug.Assert(instance.TargetProperty is not null); diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index e9ee3add9e..4936a404d4 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -228,7 +228,10 @@ namespace Avalonia return target.Bind(this, source, priority); } - internal override IDisposable RouteBind(AvaloniaObject o, BindingExpression source, BindingPriority priority) + internal override IDisposable RouteBind( + AvaloniaObject o, + UntypedBindingExpressionBase source, + BindingPriority priority) { return o.GetValueStore().AddBinding(this, source, priority); } diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index eafaab7f20..e454c64758 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -120,7 +120,7 @@ namespace Avalonia.Styling { if (i.Source is BindingExpression expression) { - expression.Initialize(target.GetValueStore(), Property, instance.Priority); + expression.Attach(target.GetValueStore(), target, Property, instance.Priority); return expression; } else diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 8ecc453443..128e2a5469 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -429,5 +429,25 @@ namespace Avalonia.Utilities return null; } + + /// + /// Determines whether the specified object instances are "identity" equal which means + /// reference equal for reference types and for value + /// types. + /// + /// The first object to compare. + /// The second object to compare. + /// + /// The type which determines whether the objects should be treated as a reference or + /// value type. + /// + /// True if the objects are considered equal; otherwise false. + internal static bool IdentityEquals(object? a, object? b, Type type) + { + if (type.IsValueType || type == typeof(string)) + return Equals(a, b); + else + return ReferenceEquals(a, b); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index d9936294ce..c7060f06aa 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -71,9 +71,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions enableDataValidation: enableDataValidation, mode: ResolveBindingMode(target, targetProperty), stringFormat: StringFormat, - target: target, targetNullValue: TargetNullValue, - targetProperty: targetProperty, targetTypeConverter: TargetTypeConverter.GetDefaultConverter()); return new InstancedBinding(expression, Mode, Priority); diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index b44625940c..61733771a0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -39,14 +39,14 @@ namespace Avalonia.Markup.Xaml.Templates { if (ItemsSource != null) { - var obs = ItemsSource switch + var expression = ItemsSource switch { Binding reflection => reflection.CreateObservableForTreeDataTemplate(item), CompiledBindingExtension compiled => compiled.CreateObservableForTreeDataTemplate(item), _ => throw new InvalidOperationException("TreeDataTemplate currently only supports Binding and CompiledBindingExtension!") }; - return InstancedBinding.OneWay(obs, BindingPriority.Style); + return new InstancedBinding(expression, BindingMode.OneWay, BindingPriority.Style); } return null; diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 1540d80540..ef021d71f5 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -105,9 +105,7 @@ namespace Avalonia.Data enableDataValidation: enableDataValidation, mode: ResolveBindingMode(target, targetProperty), stringFormat: StringFormat, - target: target, targetNullValue: TargetNullValue, - targetProperty: targetProperty, targetTypeConverter: TargetTypeConverter.GetReflectionConverter()); return new InstancedBinding(expression, Mode, Priority); } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index e483a8646c..57f64dcafa 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -175,7 +175,10 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } - internal override IDisposable RouteBind(AvaloniaObject o, BindingExpression source, BindingPriority priority) + internal override IDisposable RouteBind( + AvaloniaObject o, + UntypedBindingExpressionBase source, + BindingPriority priority) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index 67cb97044f..094c3fc708 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -15,16 +15,11 @@ namespace Avalonia.Base.UnitTests.Data.Core [InvariantCulture] public class BindingExpressionTests { - private static AvaloniaProperty TargetTypeDouble => TargetProperties.DoubleProperty; - private static AvaloniaProperty TargetTypeInt => TargetProperties.IntProperty; - private static AvaloniaProperty TargetTypeString => TargetProperties.StringProperty; - - [Fact] public async Task Should_Get_Source_Value() { var data = "foo"; - var target = BindingExpression.Create(data, o => o); + var target = BindingExpression.Create(data, o => o).ToObservable(); var result = await target.Take(1); Assert.Equal("foo", result); @@ -36,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Convert_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = BindingExpression.Create(data, o => o.StringValue, targetProperty: TargetTypeDouble); + var target = BindingExpression.Create(data, o => o.StringValue).ToObservable(typeof(double)); var result = await target.Take(1); Assert.Equal(5.6, result); @@ -48,7 +43,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Getting_Invalid_Double_String_Should_Return_BindingError() { var data = new Class1 { StringValue = "foo" }; - var target = BindingExpression.Create(data, o => o.StringValue, targetProperty: TargetTypeDouble); + var target = BindingExpression.Create(data, o => o.StringValue).ToObservable(typeof(double)); var result = await target.Take(1); Assert.IsType(result); @@ -60,11 +55,11 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Convert_Set_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = BindingExpression.Create(data, o => o.StringValue, targetProperty: TargetTypeDouble); + var target = BindingExpression.Create(data, o => o.StringValue); - using (target.Subscribe(x => { })) + using (target.ToObservable().Subscribe(x => { })) { - target.SetValue($"{6.7}"); + target.WriteValueToSource($"{6.7}"); } Assert.Equal($"{6.7}", data.StringValue); @@ -76,7 +71,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Convert_Double_To_String() { var data = new Class1 { DoubleValue = 5.6 }; - var target = BindingExpression.Create(data, o => o.DoubleValue, targetProperty: TargetTypeString); + var target = BindingExpression.Create(data, o => o.DoubleValue).ToObservable(typeof(string)); var result = await target.Take(1); Assert.Equal($"{5.6}", result); @@ -90,9 +85,9 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new Class1 { DoubleValue = 5.6 }; var target = BindingExpression.Create(data, o => o.DoubleValue); - using (target.Subscribe(x => { })) + using (target.ToObservable().Subscribe(x => { })) { - target.SetValue($"{6.7}"); + target.WriteValueToSource($"{6.7}"); } Assert.Equal(6.7, data.DoubleValue); @@ -101,14 +96,13 @@ namespace Avalonia.Base.UnitTests.Data.Core } [Fact] - public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value() + public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertible_Target_Value() { var data = new Class1 { StringValue = "foo" }; var target = BindingExpression.Create( data, o => o.StringValue, - fallbackValue: 42, - targetProperty: TargetTypeInt); + fallbackValue: 42).ToObservable(typeof(int)); var result = await target.Take(1); Assert.Equal( @@ -122,15 +116,14 @@ namespace Avalonia.Base.UnitTests.Data.Core } [Fact] - public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation() + public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertible_Target_Value_With_Data_Validation() { var data = new Class1 { StringValue = "foo" }; var target = BindingExpression.Create( data, o => o.StringValue, enableDataValidation: true, - fallbackValue: 42, - targetProperty: TargetTypeInt); + fallbackValue: 42).ToObservable(typeof(int)); var result = await target.Take(1); Assert.Equal( @@ -147,9 +140,10 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Setting_Invalid_Double_String_Should_Not_Change_Target() { var data = new Class1 { DoubleValue = 5.6 }; - var target = BindingExpression.Create(data, o => o.DoubleValue, targetProperty: TargetTypeString); + var target = BindingExpression.Create(data, o => o.DoubleValue); - target.SetValue("foo"); + using (target.ToObservable().Subscribe(_ => { })) + target.WriteValueToSource("foo"); Assert.Equal(5.6, data.DoubleValue); @@ -163,11 +157,10 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create( data, o => o.DoubleValue, - fallbackValue: "9.8", - targetProperty: TargetTypeString); + fallbackValue: "9.8"); - using (target.Subscribe(_ => { })) - target.SetValue("foo"); + using (target.ToObservable().Subscribe(_ => { })) + target.WriteValueToSource("foo"); Assert.Equal(9.8, data.DoubleValue); @@ -178,10 +171,10 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value() { var data = new Class1 { DoubleValue = 5.6 }; - var target = BindingExpression.Create(data, o => o.DoubleValue, targetProperty: TargetTypeString); + var target = BindingExpression.Create(data, o => o.DoubleValue); - using (target.Subscribe(_ => { })) - target.SetValue(AvaloniaProperty.UnsetValue); + using (target.ToObservable().Subscribe(_ => { })) + target.WriteValueToSource(AvaloniaProperty.UnsetValue); Assert.Equal(0, data.DoubleValue); @@ -198,8 +191,7 @@ namespace Avalonia.Base.UnitTests.Data.Core data, o => o.DoubleValue, converter: converter.Object, - converterParameter: "foo", - targetProperty: TargetTypeString); + converterParameter: "foo").ToObservable(typeof(string)); target.Subscribe(_ => { }); @@ -217,11 +209,10 @@ namespace Avalonia.Base.UnitTests.Data.Core data, o => o.DoubleValue, converter: converter.Object, - converterParameter: "foo", - targetProperty: TargetTypeString); + converterParameter: "foo"); - using (target.Subscribe(_ => { })) - target.SetValue("bar"); + using (target.ToObservable().Subscribe(_ => { })) + target.WriteValueToSource("bar"); converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentCulture)); @@ -236,14 +227,13 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create( data, o => o.DoubleValue, - enableDataValidation: true, - targetProperty: TargetTypeString); + enableDataValidation: true); var result = new List(); - target.Subscribe(x => result.Add(x)); - target.SetValue(1.2); - target.SetValue($"{3.4}"); - target.SetValue("bar"); + target.ToObservable(typeof(string)).Subscribe(x => result.Add(x)); + target.WriteValueToSource(1.2); + target.WriteValueToSource($"{3.4}"); + target.WriteValueToSource("bar"); Assert.Equal( new[] @@ -268,11 +258,10 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create( data, o => o.StringValue, - targetNullValue: "bar", - targetProperty: TargetTypeString); + targetNullValue: "bar"); object result = null; - target.Subscribe(x => result = x); + target.ToObservable().Subscribe(x => result = x); Assert.Equal("foo", result); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AttachedProperty.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AttachedProperty.cs index d97aec71f5..da8b5855f9 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AttachedProperty.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AttachedProperty.cs @@ -14,7 +14,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Attached_Property_Value() { var data = new Class1(); - var target = BindingExpression.Create(data, o => o[Owner.FooProperty]); + var target = BindingExpression.Create(data, o => o[Owner.FooProperty]).ToObservable(); var result = await target.Take(1); Assert.Equal("foo", result); @@ -33,7 +33,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } }; - var target = BindingExpression.Create(data, o => o.Next[Owner.FooProperty]); + var target = BindingExpression.Create(data, o => o.Next[Owner.FooProperty]).ToObservable(); var result = await target.Take(1); Assert.Equal("bar", result); @@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Simple_Attached_Value() { var data = new Class1(); - var target = BindingExpression.Create(data, o => o[Owner.FooProperty]); + var target = BindingExpression.Create(data, o => o[Owner.FooProperty]).ToObservable(); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } }; - var target = BindingExpression.Create(data, o => o.Next[Owner.FooProperty]); + var target = BindingExpression.Create(data, o => o.Next[Owner.FooProperty]).ToObservable(); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -93,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Data.Core }; var result = run(); - result.Item1.Subscribe(x => { }); + result.Item1.ToObservable().Subscribe(x => { }); GC.Collect(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AvaloniaProperty.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AvaloniaProperty.cs index 1c6c1b88f8..baa7957e7f 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AvaloniaProperty.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AvaloniaProperty.cs @@ -20,7 +20,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value() { var data = new Class1(); - var target = BindingExpression.Create(data, o => o.Foo); + var target = BindingExpression.Create(data, o => o.Foo).ToObservable(); var result = await target.Take(1); Assert.Equal("foo", result); @@ -32,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_ClrProperty_Value() { var data = new Class1(); - var target = BindingExpression.Create(data, o => o.ClrProperty); + var target = BindingExpression.Create(data, o => o.ClrProperty).ToObservable(); var result = await target.Take(1); Assert.Equal("clr-property", result); @@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Simple_Property_Value() { var data = new Class1(); - var target = BindingExpression.Create(data, o => o.Foo); + var target = BindingExpression.Create(data, o => o.Foo).ToObservable(); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -66,7 +66,7 @@ namespace Avalonia.Base.UnitTests.Data.Core }; var result = run(); - result.Item1.Subscribe(x => { }); + result.Item1.ToObservable().Subscribe(x => { }); GC.Collect(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs index b8c0ffeb2b..45ed6c7e8c 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs @@ -24,10 +24,10 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = CreateTarget(data, o => o.MustBePositive, false); var validationMessageFound = false; - target!.OfType() + target.ToObservable()!.OfType() .Where(x => x.ErrorType == BindingErrorType.DataValidationError) .Subscribe(_ => validationMessageFound = true); - target.SetValue(-5); + target.WriteValueToSource(-5); Assert.False(validationMessageFound); @@ -41,10 +41,10 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = CreateTarget(data, o => o.MustBePositive); var validationMessageFound = false; - target!.OfType() + target.ToObservable()!.OfType() .Where(x => x.ErrorType == BindingErrorType.DataValidationError) .Subscribe(_ => validationMessageFound = true); - target.SetValue(-5); + target.WriteValueToSource(-5); Assert.True(validationMessageFound); @@ -55,7 +55,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Indei_Validation_Does_Not_Subscribe_When_DataValidation_Not_Enabled() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = CreateTarget(data, o => o.MustBePositive, false); + var observer = CreateTarget(data, o => o.MustBePositive, false).ToObservable(); observer.Subscribe(_ => { }); @@ -66,7 +66,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Enabled_Indei_Validation_Subscribes() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = CreateTarget(data, o => o.MustBePositive); + var observer = CreateTarget(data, o => o.MustBePositive).ToObservable(); var sub = observer.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); @@ -83,11 +83,11 @@ namespace Avalonia.Base.UnitTests.Data.Core var observer = CreateTarget(data, o => o.MustBePositive); var result = new List(); - observer.Subscribe(x => result.Add(x)); - observer.SetValue(5); - observer.SetValue(-5); - observer.SetValue("foo"); - observer.SetValue(5); + observer.ToObservable().Subscribe(x => result.Add(x)); + observer.WriteValueToSource(5); + observer.WriteValueToSource(-5); + observer.WriteValueToSource("foo"); + observer.WriteValueToSource(5); Assert.Equal(new[] { @@ -120,7 +120,7 @@ namespace Avalonia.Base.UnitTests.Data.Core Inner = new IndeiTest() }; - var observer = CreateTarget(data, o => o.Inner!.MustBePositive); + var observer = CreateTarget(data, o => o.Inner!.MustBePositive).ToObservable(); observer.Subscribe(_ => { }); @@ -136,7 +136,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var container = new Container(); - var observer = CreateTarget(container, o => o.Inner!.MustBePositive); + var observer = CreateTarget(container, o => o.Inner!.MustBePositive).ToObservable(); var result = new List(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_ExpressionTree.cs index 287888a140..f26ed24764 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_ExpressionTree.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_ExpressionTree.cs @@ -15,7 +15,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var target = new object(); - var observer = BindingExpression.Create(target, o => o); + var observer = BindingExpression.Create(target, o => o).ToObservable(); Assert.Equal(target, await observer.Take(1)); GC.KeepAlive(target); @@ -26,7 +26,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var target = new Class1(); - var observer = BindingExpression.Create(target, o => o.Foo); + var observer = BindingExpression.Create(target, o => o.Foo).ToObservable(); Assert.Null(await observer.Take(1)); @@ -46,9 +46,9 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new Class1(); var target = BindingExpression.Create(data, o => o.Foo); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue("baz")); + Assert.True(target.WriteValueToSource("baz")); } GC.KeepAlive(data); @@ -59,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new[] { 1, 2, 3, 4 }; - var target = BindingExpression.Create(data, o => o[0]); + var target = BindingExpression.Create(data, o => o[0]).ToObservable(); Assert.Equal(data[0], await target.Take(1)); GC.KeepAlive(data); @@ -70,7 +70,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new List { 1, 2, 3, 4 }; - var target = BindingExpression.Create(data, o => o[0]); + var target = BindingExpression.Create(data, o => o[0]).ToObservable(); Assert.Equal(data[0], await target.Take(1)); GC.KeepAlive(data); @@ -85,7 +85,7 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Add(key, new object()); - var target = BindingExpression.Create(data, o => o[key]); + var target = BindingExpression.Create(data, o => o[key]).ToObservable(); Assert.Equal(data[key], await target.Take(1)); @@ -99,9 +99,9 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o[0]); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue(2)); + Assert.True(target.WriteValueToSource(2)); } GC.KeepAlive(data); @@ -112,7 +112,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { NotifyingBase test = new Class1 { Foo = "Test" }; - var target = BindingExpression.Create(test, o => ((Class1)o).Foo); + var target = BindingExpression.Create(test, o => ((Class1)o).Foo).ToObservable(); Assert.Equal("Test", await target.Take(1)); @@ -132,7 +132,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { NotifyingBase test = new Class1 { Foo = "Test" }; - var target = BindingExpression.Create(test, o => (o as Class1).Foo); + var target = BindingExpression.Create(test, o => (o as Class1).Foo).ToObservable(); Assert.Equal("Test", await target.Take(1)); @@ -144,7 +144,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var test = new Class2(); - var target = BindingExpression.Create(test, o => o[Class2.FooProperty]); + var target = BindingExpression.Create(test, o => o[Class2.FooProperty]).ToObservable(); Assert.Equal("foo", await target.Take(1)); @@ -156,7 +156,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var test = new Class1 { Foo = "Test" }; - var target = BindingExpression.Create(test, o => o.Foo.Length); + var target = BindingExpression.Create(test, o => o.Foo.Length).ToObservable(); Assert.Equal(test.Foo.Length, await target.Take(1)); @@ -169,7 +169,7 @@ namespace Avalonia.Base.UnitTests.Data.Core using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = Task.FromResult("foo") }; - var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); + var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()).ToObservable(); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -184,7 +184,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Create_Method_Binding() { var data = new Class3(); - var target = BindingExpression.Create(data, o => (Action)o.Method); + var target = BindingExpression.Create(data, o => (Action)o.Method).ToObservable(); var value = await target.Take(1); Assert.IsAssignableFrom(value); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Indexer.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Indexer.cs index e087029b2a..c78c366dd8 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Indexer.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Indexer.cs @@ -27,7 +27,7 @@ public abstract class BindingExpressionTests_Indexer public async Task Should_Get_Array_Value() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = CreateTarget(data, o => o.Foo[1]); + var target = CreateTarget(data, o => o.Foo[1]).ToObservable(); var result = await target.Take(1); Assert.Equal("bar", result); @@ -39,7 +39,7 @@ public abstract class BindingExpressionTests_Indexer public async Task Should_Get_MultiDimensional_Array_Value() { var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; - var target = CreateTarget(data, o => o.Foo[1, 1]); + var target = CreateTarget(data, o => o.Foo[1, 1]).ToObservable(); var result = await target.Take(1); Assert.Equal("qux", result); @@ -51,7 +51,7 @@ public abstract class BindingExpressionTests_Indexer public async Task Should_Get_Value_For_String_Indexer() { var data = new { Foo = new Dictionary { { "foo", "bar" }, { "baz", "qux" } } }; - var target = CreateTarget(data, o => o.Foo["foo"]); + var target = CreateTarget(data, o => o.Foo["foo"]).ToObservable(); var result = await target.Take(1); Assert.Equal("bar", result); @@ -63,7 +63,7 @@ public abstract class BindingExpressionTests_Indexer public async Task Should_Get_Value_For_Non_String_Indexer() { var data = new { Foo = new Dictionary { { 1.0, "bar" }, { 2.0, "qux" } } }; - var target = CreateTarget(data, o => o.Foo[1.0]); + var target = CreateTarget(data, o => o.Foo[1.0]).ToObservable(); var result = await target.Take(1); Assert.Equal("bar", result); @@ -75,7 +75,7 @@ public abstract class BindingExpressionTests_Indexer public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = CreateTarget(data, o => o.Foo[2]); + var target = CreateTarget(data, o => o.Foo[2]).ToObservable(); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); @@ -87,7 +87,7 @@ public abstract class BindingExpressionTests_Indexer public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new List { "foo", "bar" } }; - var target = CreateTarget(data, o => o.Foo[2]); + var target = CreateTarget(data, o => o.Foo[2]).ToObservable(); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); @@ -99,7 +99,7 @@ public abstract class BindingExpressionTests_Indexer public async Task Should_Get_List_Value() { var data = new { Foo = new List { "foo", "bar" } }; - var target = CreateTarget(data, o => o.Foo[1]); + var target = CreateTarget(data, o => o.Foo[1]).ToObservable(); var result = await target.Take(1); Assert.Equal("bar", result); @@ -111,7 +111,7 @@ public abstract class BindingExpressionTests_Indexer public void Should_Track_INCC_Add() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = CreateTarget(data, o => o.Foo[2]); + var target = CreateTarget(data, o => o.Foo[2]).ToObservable(); var result = new List(); using (var sub = target.Subscribe(x => result.Add(BindingNotification.ExtractValue(x)))) @@ -132,7 +132,7 @@ public abstract class BindingExpressionTests_Indexer public void Should_Track_INCC_Remove() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = CreateTarget(data, o => o.Foo[0]); + var target = CreateTarget(data, o => o.Foo[0]).ToObservable(); var result = new List(); using (var sub = target.Subscribe(result.Add)) @@ -153,7 +153,7 @@ public abstract class BindingExpressionTests_Indexer public void Should_Track_INCC_Replace() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = CreateTarget(data, o => o.Foo[1]); + var target = CreateTarget(data, o => o.Foo[1]).ToObservable(); var result = new List(); using (var sub = target.Subscribe(result.Add)) @@ -177,7 +177,7 @@ public abstract class BindingExpressionTests_Indexer // method, but even if it did we need to test with ObservableCollection as well // as AvaloniaList as it implements PropertyChanged as an explicit interface event. var data = new { Foo = new ObservableCollection { "foo", "bar" } }; - var target = CreateTarget(data, o => o.Foo[1]); + var target = CreateTarget(data, o => o.Foo[1]).ToObservable(); var result = new List(); var sub = target.Subscribe(result.Add); @@ -193,7 +193,7 @@ public abstract class BindingExpressionTests_Indexer public void Should_Track_INCC_Reset() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = CreateTarget(data, o => o.Foo[1]); + var target = CreateTarget(data, o => o.Foo[1]).ToObservable(); var result = new List(); var sub = target.Subscribe(x => result.Add(BindingNotification.ExtractValue(x))); @@ -212,7 +212,7 @@ public abstract class BindingExpressionTests_Indexer data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = CreateTarget(data, o => o.Foo["foo"]); + var target = CreateTarget(data, o => o.Foo["foo"]).ToObservable(); var result = new List(); using (var sub = target.Subscribe(result.Add)) @@ -236,9 +236,9 @@ public abstract class BindingExpressionTests_Indexer var data = new { Foo = new[] { "foo", "bar" } }; var target = CreateTarget(data, o => o.Foo[1]); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue("baz")); + Assert.True(target.WriteValueToSource("baz")); } Assert.Equal("baz", data.Foo[1]); @@ -258,9 +258,9 @@ public abstract class BindingExpressionTests_Indexer }; var target = CreateTarget(data, o => o.Foo["foo"]); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue(4)); + Assert.True(target.WriteValueToSource(4)); } Assert.Equal(4, data.Foo["foo"]); @@ -280,9 +280,9 @@ public abstract class BindingExpressionTests_Indexer }; var target = CreateTarget(data, o => o.Foo["bar"]); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue(4)); + Assert.True(target.WriteValueToSource(4)); } Assert.Equal(4, data.Foo["bar"]); @@ -299,9 +299,9 @@ public abstract class BindingExpressionTests_Indexer var target = CreateTarget(data, o => o.Foo["foo"]); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue("bar2")); + Assert.True(target.WriteValueToSource("bar2")); } Assert.Equal("bar2", data.Foo["foo"]); @@ -314,7 +314,7 @@ public abstract class BindingExpressionTests_Indexer { var data = new[] { 1, 2, 3 }; - var target = BindingExpression.Create(data, o => o[1]); + var target = BindingExpression.Create(data, o => o[1]).ToObservable(); var value = await target.Take(1); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Negation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Negation.cs index 461e8947a0..0ba7d7dd5e 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Negation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Negation.cs @@ -16,7 +16,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Negate_Boolean_Value(bool value) { var data = new Test { Foo = value }; - var target = BindingExpression.Create(data, o => !o.Foo); + var target = BindingExpression.Create(data, o => !o.Foo).ToObservable(); var result = await target.Take(1); Assert.Equal(!value, (bool)result); @@ -30,7 +30,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Negate_Boolean_Value_In_Path(bool value) { var data = new Test { Next = new Test { Foo = value } }; - var target = BindingExpression.Create(data, o => !o.Next!.Foo); + var target = BindingExpression.Create(data, o => !o.Next!.Foo).ToObservable(); var result = await target.Take(1); Assert.Equal(!value, (bool)result); @@ -44,7 +44,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Double_Negate_Boolean_Value(bool value) { var data = new Test { Foo = value }; - var target = BindingExpression.Create(data, o => !!o.Foo); + var target = BindingExpression.Create(data, o => !!o.Foo).ToObservable(); var result = await target.Take(1); Assert.Equal(value, (bool)result); @@ -58,7 +58,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Double_Negate_Boolean_Value_In_Path(bool value) { var data = new Test { Next = new Test { Foo = value } }; - var target = BindingExpression.Create(data, o => !!o.Next!.Foo); + var target = BindingExpression.Create(data, o => !!o.Next!.Foo).ToObservable(); var result = await target.Take(1); Assert.Equal(value, (bool)result); @@ -71,9 +71,9 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Test { Foo = true }; var target = BindingExpression.Create(data, o => !o.Foo); - target.Subscribe(_ => { }); + target.ToObservable().Subscribe(_ => { }); - Assert.True(target.SetValue(true)); + Assert.True(target.WriteValueToSource(true)); Assert.False(data.Foo); } @@ -83,9 +83,9 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Test { Next = new Test { Foo = true } }; var target = BindingExpression.Create(data, o => !o.Next!.Foo); - target.Subscribe(_ => { }); + target.ToObservable().Subscribe(_ => { }); - Assert.True(target.SetValue(true)); + Assert.True(target.WriteValueToSource(true)); Assert.False(data.Next.Foo); } @@ -95,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Test { Foo = true }; var target = BindingExpression.Create(data, o => !!o.Foo); - target.Subscribe(_ => { }); + target.ToObservable().Subscribe(_ => { }); - Assert.True(target.SetValue(false)); + Assert.True(target.WriteValueToSource(false)); Assert.False(data.Foo); } @@ -107,9 +107,9 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Test { Next = new Test { Foo = true } }; var target = BindingExpression.Create(data, o => !!o.Next!.Foo); - target.Subscribe(_ => { }); + target.ToObservable().Subscribe(_ => { }); - Assert.True(target.SetValue(false)); + Assert.True(target.WriteValueToSource(false)); Assert.False(data.Next.Foo); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Observable.cs index 5cc3c26139..ad2fb02a4e 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Observable.cs @@ -22,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); source.OnNext("bar"); sync.ExecutePostedCallbacks(); @@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); source.OnNext("bar"); sync.ExecutePostedCallbacks(); @@ -61,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Next.StreamBinding().Foo); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); data.Next.OnNext(new Class2("foo")); sync.ExecutePostedCallbacks(); @@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); source.OnNext("bar"); sync.ExecutePostedCallbacks(); @@ -108,7 +108,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data1, o => o.Next.StreamBinding().Foo, enableDataValidation: true); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); data1.Next.OnNext(data2); sync.ExecutePostedCallbacks(); @@ -138,7 +138,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var result = new List(); - var sub = target.Subscribe(x => result.Add((int)x)); + var sub = target.ToObservable().Subscribe(x => result.Add((int)x)); source.OnNext(42); sync.ExecutePostedCallbacks(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Property.cs index 45e7379a22..9480af969f 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Property.cs @@ -22,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value() { var data = new { Foo = "foo" }; - var target = BindingExpression.Create(data, o => o.Foo); + var target = BindingExpression.Create(data, o => o.Foo).ToObservable(); var result = await target.Take(1); Assert.Equal("foo", result); @@ -34,7 +34,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value_Null() { var data = new { Foo = (string)null }; - var target = BindingExpression.Create(data, o => o.Foo); + var target = BindingExpression.Create(data, o => o.Foo).ToObservable(); var result = await target.Take(1); Assert.Null(result); @@ -46,7 +46,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_From_Base_Class() { var data = new Class3 { Foo = "foo" }; - var target = BindingExpression.Create(data, o => o.Foo); + var target = BindingExpression.Create(data, o => o.Foo).ToObservable(); var result = await target.Take(1); Assert.Equal("foo", result); @@ -57,7 +57,7 @@ namespace Avalonia.Base.UnitTests.Data.Core [Fact] public async Task Should_Return_BindingNotification_Error_For_Root_Null() { - var target = BindingExpression.Create(default(Class3), o => o.Foo); + var target = BindingExpression.Create(default(Class3), o => o.Foo).ToObservable(); var result = await target.Take(1); Assert.Equal( @@ -71,7 +71,7 @@ namespace Avalonia.Base.UnitTests.Data.Core [Fact] public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue() { - var target = BindingExpression.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo); + var target = BindingExpression.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo).ToObservable(); var result = await target.Take(1); Assert.Equal( @@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Chain() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; - var target = BindingExpression.Create(data, o => o.Foo.Bar.Baz); + var target = BindingExpression.Create(data, o => o.Foo.Bar.Baz).ToObservable(); var result = await target.Take(1); Assert.Equal("baz", result); @@ -101,7 +101,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo.Foo.Length); var result = new List(); - target.Subscribe(x => result.Add(x)); + target.ToObservable().Subscribe(x => result.Add(x)); Assert.Equal( new[] @@ -123,7 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); data.Foo = "bar"; Assert.Equal(new[] { "foo", "bar" }, result); @@ -145,7 +145,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Bar); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); Assert.Equal(new[] { "foo" }, result); @@ -182,7 +182,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => (o.Next as Class2).Bar); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); ((Class2)data.Next).Bar = "baz"; ((Class2)data.Next).Bar = null; @@ -205,7 +205,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => (o.Next as Class2).Bar); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); var old = data.Next; data.Next = new Class2 { Bar = "baz" }; data.Next = new Class2 { Bar = null }; @@ -241,7 +241,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => ((o.Next as Class2).Next as Class2).Bar); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); var old = data.Next; data.Next = new Class2 { Bar = "baz" }; data.Next = old; @@ -277,7 +277,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => (o.Next as Class2).Bar); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); var old = data.Next; var breaking = new WithoutBar(); data.Next = breaking; @@ -321,7 +321,7 @@ namespace Avalonia.Base.UnitTests.Data.Core }; var result = run(); - result.Item1.Subscribe(x => { }); + result.Item1.ToObservable().Subscribe(x => { }); // Mono trickery GC.Collect(2); @@ -341,7 +341,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(source, x => x.Name); var result = new List(); - target.Subscribe(x => result.Add(x)); + target.ToObservable().Subscribe(x => result.Add(x)); Assert.Equal(new[] { "NewName" }, result); } @@ -357,8 +357,6 @@ namespace Avalonia.Base.UnitTests.Data.Core data, o => o.IntValue, mode: BindingMode.TwoWay, - target: control, - targetProperty: DockPanel.DockProperty, allowReflection: allowReflection); var instance = new InstancedBinding( target, diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_SetValue.cs index e800c2df2f..8a95e9a722 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_SetValue.cs @@ -21,9 +21,9 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new Person { Name = "Frank" }; var target = BindingExpression.Create(data, o => o.Name); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - target.SetValue("Kups"); + target.WriteValueToSource("Kups"); } Assert.Equal("Kups", data.Name); @@ -35,9 +35,9 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new AvaloniaObject(); var target = BindingExpression.Create(data, o => o[DockPanel.DockProperty]); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - target.SetValue(Dock.Right); + target.WriteValueToSource(Dock.Right); } Assert.Equal(Dock.Right, data[DockPanel.DockProperty]); @@ -49,9 +49,9 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new { Foo = new[] { "foo" } }; var target = BindingExpression.Create(data, o => o.Foo[0]); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - target.SetValue("bar"); + target.WriteValueToSource("bar"); } Assert.Equal("bar", data.Foo[0]); @@ -65,9 +65,9 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new Person { Pet = new Dog { Name = "Fido" } }; var target = BindingExpression.Create(data, o => o.Pet!.Name); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - target.SetValue("Rover"); + target.WriteValueToSource("Rover"); } Assert.Equal("Rover", data.Pet.Name); @@ -80,10 +80,10 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Pet!.Name); // Ensure the UntypedBindingExpression's subscriptions are kept active. - using (target!.OfType().Subscribe(x => { })) + using (target.ToObservable()!.OfType().Subscribe(x => { })) { data.Pet = null; - Assert.False(target.SetValue("Rover")); + Assert.False(target.WriteValueToSource("Rover")); } } @@ -93,9 +93,9 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new Person { Pet = new Cat() }; var target = BindingExpression.Create(data, o => (o.Pet as Dog)!.IsBarky); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.False(target.SetValue("baz")); + Assert.False(target.WriteValueToSource("baz")); } GC.KeepAlive(data); @@ -108,8 +108,8 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Name); var result = new List(); - target.Subscribe(result.Add); - target.SetValue("Frank"); + target.ToObservable().Subscribe(result.Add); + target.WriteValueToSource("Frank"); Assert.Equal(new[] { null, "Frank" }, result); @@ -123,8 +123,8 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Name); var result = new List(); - target.Subscribe(result.Add); - target.SetValue("Frank"); + target.ToObservable().Subscribe(result.Add); + target.WriteValueToSource("Frank"); Assert.Equal(new[] { null, "Frank" }, result); @@ -137,9 +137,9 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new Person(); var target = BindingExpression.Create(data, o => (o.Pet as Dog)!.Name); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.False(target.SetValue("Fido")); + Assert.False(target.WriteValueToSource("Fido")); } GC.KeepAlive(data); @@ -154,9 +154,9 @@ namespace Avalonia.Base.UnitTests.Data.Core o => o.Name, converter: new CaseConverter()); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue("Kups")); + Assert.True(target.WriteValueToSource("Kups")); } Assert.Equal("kups", data.Name); @@ -173,9 +173,7 @@ namespace Avalonia.Base.UnitTests.Data.Core data, o => o.WhiskerCount, converter: new CaseConverter(), - mode: BindingMode.TwoWay, - target: control, - targetProperty: Visual.OpacityProperty); + mode: BindingMode.TwoWay); var instance = new InstancedBinding( target, BindingMode.TwoWay, diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs index a98ab7d327..7a8e54b8a5 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs @@ -21,7 +21,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); tcs.SetResult("foo"); sync.ExecutePostedCallbacks(); @@ -41,7 +41,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); Assert.Equal(new[] { "foo" }, result); @@ -59,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Next.StreamBinding().Foo); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); tcs.SetResult(new Class2("foo")); sync.ExecutePostedCallbacks(); @@ -79,7 +79,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); tcs.SetException(new NotSupportedException()); sync.ExecutePostedCallbacks(); @@ -105,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); Assert.Equal( new[] @@ -130,7 +130,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); tcs.SetResult("foo"); sync.ExecutePostedCallbacks(); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 660c7e8511..47280b859d 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1847,7 +1847,7 @@ namespace Avalonia.Controls.UnitTests public InstancedBinding ItemsSelector(object item) { var obs = BindingExpression.Create(item, o => ((Node)o).Children); - return InstancedBinding.OneWay(obs); + return new InstancedBinding(obs, BindingMode.OneWay, BindingPriority.LocalValue); } public bool Match(object? data) diff --git a/tests/Avalonia.LeakTests/BindingExpressionTests.cs b/tests/Avalonia.LeakTests/BindingExpressionTests.cs index 034e0a6d54..4b38cfe384 100644 --- a/tests/Avalonia.LeakTests/BindingExpressionTests.cs +++ b/tests/Avalonia.LeakTests/BindingExpressionTests.cs @@ -26,7 +26,7 @@ namespace Avalonia.LeakTests var source = new { Foo = new AvaloniaList { "foo", "bar" } }; var target = BindingExpression.Create(source, o => o.Foo); - target.Subscribe(_ => { }); + target.ToObservable().Subscribe(_ => { }); return target; }; @@ -44,7 +44,7 @@ namespace Avalonia.LeakTests var source = new { Foo = new AvaloniaList { "foo", "bar" } }; var target = BindingExpression.Create(source, o => o.Foo, enableDataValidation: true); - target.Subscribe(_ => { }); + target.ToObservable().Subscribe(_ => { }); return target; }; @@ -62,7 +62,7 @@ namespace Avalonia.LeakTests var source = new { Foo = new NonIntegerIndexer() }; var target = BindingExpression.Create(source, o => o.Foo); - target.Subscribe(_ => { }); + target.ToObservable().Subscribe(_ => { }); return target; }; @@ -79,7 +79,7 @@ namespace Avalonia.LeakTests { var source = new { Foo = new MethodBound() }; var target = BindingExpression.Create(source, o => (Action)o.Foo.A); - target.Subscribe(_ => { }); + target.ToObservable().Subscribe(_ => { }); return target; }; diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 6dc36d6811..d4a799da6f 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -313,10 +313,10 @@ namespace Avalonia.Markup.UnitTests.Data child.GetObservable(Control.DataContextProperty).Subscribe(x => values.Add(x)); child.Bind(Control.DataContextProperty, new Binding("Foo")); - // When binding to DataContext and the target isn't found, the binding should produce + // When binding to DataContext and the source isn't found, the binding should produce // null rather than UnsetValue in order to not propagate incorrect DataContexts from // parent controls while things are being set up. This logic is implemented in - // `Avalonia.Markup.Data.Binding.Initiate`. + // `UntypedBindingExpressionBase.PublishValue`. Assert.True(child.IsSet(Control.DataContextProperty)); root.Child = child; diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs index f23e48d148..cd1c45b513 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs @@ -26,7 +26,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TextProperty).Source; + TextBlock.TextProperty).Expression; Assert.Same(StringConverters.IsNullOrEmpty, expressionObserver.Converter); } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs index d214af51d9..cc53a85a4f 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs @@ -109,7 +109,7 @@ namespace Avalonia.Markup.UnitTests.Parsers [Fact] public void Should_Not_Keep_Source_Alive() { - Func> run = () => + Func, WeakReference>> run = () => { var source = new Class1(); var target = Build(source, "(Owner.Foo)", typeResolver: _typeResolver); @@ -144,13 +144,13 @@ namespace Avalonia.Markup.UnitTests.Parsers Assert.Throws(() => Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver)); } - private static BindingExpression Build(object source, string path, Func typeResolver) + private static IObservable Build(object source, string path, Func typeResolver) { var r = new CharacterReader(path); var grammar = BindingExpressionGrammar.Parse(ref r).Nodes; var nodes = new List(); ExpressionNodeFactory.CreateFromAst(grammar, typeResolver, null, nodes, out _); - return new BindingExpression(source, nodes, AvaloniaProperty.UnsetValue); + return new BindingExpression(source, nodes, AvaloniaProperty.UnsetValue).ToObservable(); } private static class Owner diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs index c2b05f6f74..5bda7350c2 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs @@ -22,7 +22,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Get_AvaloniaProperty_By_Name() { var data = new Class1(); - var target = Build(data, "Foo"); + var target = Build(data, "Foo").ToObservable(); var result = await target.Take(1); Assert.Equal("foo", result); @@ -37,7 +37,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var target = Build(data, "Foo"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x)); + var sub = target.ToObservable().Subscribe(x => result.Add(x)); data.SetValue(Class1.FooProperty, "bar"); Assert.Equal(new[] { "foo", "bar" }, result); diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs index f87a63528b..7ae3f9e5c4 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs @@ -22,7 +22,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Get_Array_Value() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = Build(data, "Foo[1]"); + var target = BuildAsObservable(data, "Foo[1]"); var result = await target.Take(1); Assert.Equal("bar", result); @@ -34,7 +34,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Get_UnsetValue_For_Invalid_Array_Index() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = Build(data, "Foo[invalid]"); + var target = BuildAsObservable(data, "Foo[invalid]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); @@ -46,7 +46,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index() { var data = new { Foo = new Dictionary { { 1, "foo" } } }; - var target = Build(data, "Foo[invalid]"); + var target = BuildAsObservable(data, "Foo[invalid]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); @@ -58,7 +58,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Get_Error_For_Object_Without_Indexer() { var data = new { Foo = 5 }; - var target = Build(data, "Foo[noindexer]"); + var target = BuildAsObservable(data, "Foo[noindexer]"); var result = await target.Take(1); Assert.Equal( @@ -74,7 +74,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Get_MultiDimensional_Array_Value() { var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; - var target = Build(data, "Foo[1, 1]"); + var target = BuildAsObservable(data, "Foo[1, 1]"); var result = await target.Take(1); Assert.Equal("qux", result); @@ -86,7 +86,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Get_Value_For_String_Indexer() { var data = new { Foo = new Dictionary { { "foo", "bar" }, { "baz", "qux" } } }; - var target = Build(data, "Foo[foo]"); + var target = BuildAsObservable(data, "Foo[foo]"); var result = await target.Take(1); Assert.Equal("bar", result); @@ -98,7 +98,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Get_Value_For_Non_String_Indexer() { var data = new { Foo = new Dictionary { { 1.0, "bar" }, { 2.0, "qux" } } }; - var target = Build(data, "Foo[1.0]"); + var target = BuildAsObservable(data, "Foo[1.0]"); var result = await target.Take(1); Assert.Equal("bar", result); @@ -110,7 +110,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = Build(data, "Foo[2]"); + var target = BuildAsObservable(data, "Foo[2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); @@ -122,7 +122,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = Build(data, "Foo[1,2]"); + var target = BuildAsObservable(data, "Foo[1,2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); @@ -134,7 +134,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new List { "foo", "bar" } }; - var target = Build(data, "Foo[2]"); + var target = BuildAsObservable(data, "Foo[2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); @@ -146,7 +146,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Get_List_Value() { var data = new { Foo = new List { "foo", "bar" } }; - var target = Build(data, "Foo[1]"); + var target = BuildAsObservable(data, "Foo[1]"); var result = await target.Take(1); Assert.Equal("bar", result); @@ -158,7 +158,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public void Should_Track_INCC_Add() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = Build(data, "Foo[2]"); + var target = BuildAsObservable(data, "Foo[2]"); var result = new List(); using (var sub = target.Subscribe(x => result.Add(BindingNotification.ExtractValue(x)))) @@ -179,7 +179,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public void Should_Track_INCC_Remove() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = Build(data, "Foo[0]"); + var target = BuildAsObservable(data, "Foo[0]"); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -200,7 +200,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public void Should_Track_INCC_Replace() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = Build(data, "Foo[1]"); + var target = BuildAsObservable(data, "Foo[1]"); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -224,7 +224,7 @@ namespace Avalonia.Markup.UnitTests.Parsers // method, but even if it did we need to test with ObservableCollection as well // as AvaloniaList as it implements PropertyChanged as an explicit interface event. var data = new { Foo = new ObservableCollection { "foo", "bar" } }; - var target = Build(data, "Foo[1]"); + var target = BuildAsObservable(data, "Foo[1]"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -240,7 +240,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public void Should_Track_INCC_Reset() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = Build(data, "Foo[1]"); + var target = BuildAsObservable(data, "Foo[1]"); var result = new List(); var sub = target.Subscribe(x => result.Add(BindingNotification.ExtractValue(x))); @@ -259,7 +259,7 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = Build(data, "Foo[foo]"); + var target = BuildAsObservable(data, "Foo[foo]"); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -283,9 +283,9 @@ namespace Avalonia.Markup.UnitTests.Parsers var data = new { Foo = new[] { "foo", "bar" } }; var target = Build(data, "Foo[1]"); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue("baz")); + Assert.True(target.WriteValueToSource("baz")); } Assert.Equal("baz", data.Foo[1]); @@ -305,9 +305,9 @@ namespace Avalonia.Markup.UnitTests.Parsers }; var target = Build(data, "Foo[foo]"); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue(4)); + Assert.True(target.WriteValueToSource(4)); } Assert.Equal(4, data.Foo["foo"]); @@ -327,9 +327,9 @@ namespace Avalonia.Markup.UnitTests.Parsers }; var target = Build(data, "Foo[bar]"); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue(4)); + Assert.True(target.WriteValueToSource(4)); } Assert.Equal(4, data.Foo["bar"]); @@ -346,9 +346,9 @@ namespace Avalonia.Markup.UnitTests.Parsers var target = Build(data, "Foo[foo]"); - using (target.Subscribe(_ => { })) + using (target.ToObservable().Subscribe(_ => { })) { - Assert.True(target.SetValue("bar2")); + Assert.True(target.WriteValueToSource("bar2")); } Assert.Equal("bar2", data.Foo["foo"]); @@ -361,7 +361,7 @@ namespace Avalonia.Markup.UnitTests.Parsers { var data = new[] { 1, 2, 3 }; - var target = Build(data, "[1]"); + var target = BuildAsObservable(data, "[1]"); var value = await target.Take(1); @@ -377,6 +377,11 @@ namespace Avalonia.Markup.UnitTests.Parsers return new BindingExpression(source, nodes, AvaloniaProperty.UnsetValue); } + private static IObservable BuildAsObservable(object source, string path, Func typeResolver = null) + { + return Build(source, path, typeResolver).ToObservable(); + } + private class NonIntegerIndexer : NotifyingBase { private readonly Dictionary _storage = new Dictionary(); diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs index af4ce51f82..e9b83584e7 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs @@ -69,13 +69,13 @@ namespace Avalonia.Markup.UnitTests.Parsers } - private static BindingExpression Build(object source, string path) + private static IObservable Build(object source, string path) { var r = new CharacterReader(path); var grammar = BindingExpressionGrammar.Parse(ref r).Nodes; var nodes = new List(); ExpressionNodeFactory.CreateFromAst(grammar, null, null, nodes, out _); - return new BindingExpression(source, nodes, AvaloniaProperty.UnsetValue); + return new BindingExpression(source, nodes, AvaloniaProperty.UnsetValue).ToObservable(); } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs index 07dfb16c91..973edc9456 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs @@ -19,7 +19,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Negate_0() { var data = new { Foo = 0 }; - var target = Build(data, "!Foo"); + var target = BuildAsObservable(data, "!Foo"); var result = await target.Take(1); Assert.True((bool)result); @@ -31,7 +31,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Negate_1() { var data = new { Foo = 1 }; - var target = Build(data, "!Foo"); + var target = BuildAsObservable(data, "!Foo"); var result = await target.Take(1); Assert.False((bool)result); @@ -43,7 +43,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Negate_False_String() { var data = new { Foo = "false" }; - var target = Build(data, "!Foo"); + var target = BuildAsObservable(data, "!Foo"); var result = await target.Take(1); Assert.True((bool)result); @@ -55,7 +55,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Negate_True_String() { var data = new { Foo = "True" }; - var target = Build(data, "!Foo"); + var target = BuildAsObservable(data, "!Foo"); var result = await target.Take(1); Assert.False((bool)result); @@ -67,7 +67,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() { var data = new { Foo = "foo" }; - var target = Build(data, "!Foo"); + var target = BuildAsObservable(data, "!Foo"); var result = await target.Take(1); Assert.Equal( @@ -83,7 +83,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() { var data = new { Foo = new object() }; - var target = Build(data, "!Foo"); + var target = BuildAsObservable(data, "!Foo"); var result = await target.Take(1); Assert.Equal( @@ -99,7 +99,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Negate_BindingNotification_Value() { var data = new { Foo = true }; - var target = Build(data, "!Foo", enableDataValidation: true); + var target = BuildAsObservable(data, "!Foo", enableDataValidation: true); var result = await target.Take(1); Assert.Equal(new BindingNotification(false), result); @@ -111,7 +111,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Pass_Through_BindingNotification_Error() { var data = new object(); - var target = Build(data, "!Foo", enableDataValidation: true); + var target = BuildAsObservable(data, "!Foo", enableDataValidation: true); var result = await target.Take(1); Assert.Equal( @@ -127,7 +127,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Negate_BindingNotification_Error_FallbackValue() { var data = new Test { DataValidationError = "Test error" }; - var target = Build(data, "!Foo", enableDataValidation: true); + var target = BuildAsObservable(data, "!Foo", enableDataValidation: true); var result = await target.Take(1); Assert.Equal( @@ -145,9 +145,9 @@ namespace Avalonia.Markup.UnitTests.Parsers { var data = new { Foo = "foo" }; var target = Build(data, "!Foo"); - target.Subscribe(_ => { }); + target.ToObservable().Subscribe(_ => { }); - Assert.False(target.SetValue("bar")); + Assert.False(target.WriteValueToSource("bar")); GC.KeepAlive(data); } @@ -165,6 +165,11 @@ namespace Avalonia.Markup.UnitTests.Parsers enableDataValidation: enableDataValidation); } + private static IObservable BuildAsObservable(object source, string path, bool enableDataValidation = false) + { + return Build(source, path, enableDataValidation).ToObservable(); + } + private class Test : INotifyDataErrorInfo { private string _dataValidationError; diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs index 6fcf0d3e73..9c30b98286 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Should_Return_BindingNotification_Error_For_Broken_Chain() { var data = new { Foo = new { Bar = 1 } }; - var target = Build(data, "Foo.Bar.Baz"); + var target = Build(data, "Foo.Bar.Baz").ToObservable(); var result = await target.Take(1); Assert.IsType(result);