Browse Source

Introduce BindingExpressionBase.

And `UntypedBindingExpressionBase`.
refactor/bindingexpressions-in-valuestore
Steven Kirk 3 years ago
parent
commit
dbfde2dcc5
  1. 2
      src/Avalonia.Base/AvaloniaObject.cs
  2. 2
      src/Avalonia.Base/AvaloniaProperty.cs
  3. 14
      src/Avalonia.Base/Data/BindingNotification.cs
  4. 2
      src/Avalonia.Base/Data/BindingOperations.cs
  5. 18
      src/Avalonia.Base/Data/Core/BindingError.cs
  6. 56
      src/Avalonia.Base/Data/Core/BindingExpression.Observable.cs
  7. 33
      src/Avalonia.Base/Data/Core/BindingExpression.ValueEntry.cs
  8. 383
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  9. 10
      src/Avalonia.Base/Data/Core/BindingExpressionBase.cs
  10. 19
      src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs
  11. 24
      src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs
  12. 455
      src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs
  13. 17
      src/Avalonia.Base/Data/InstancedBinding.cs
  14. 5
      src/Avalonia.Base/DirectPropertyBase.cs
  15. 4
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  16. 27
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  17. 5
      src/Avalonia.Base/StyledProperty.cs
  18. 2
      src/Avalonia.Base/Styling/Setter.cs
  19. 20
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  20. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  21. 4
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  22. 2
      src/Markup/Avalonia.Markup/Data/Binding.cs
  23. 5
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  24. 77
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  25. 10
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AttachedProperty.cs
  26. 8
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_AvaloniaProperty.cs
  27. 26
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs
  28. 30
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_ExpressionTree.cs
  29. 44
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Indexer.cs
  30. 24
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Negation.cs
  31. 12
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Observable.cs
  32. 32
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Property.cs
  33. 44
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_SetValue.cs
  34. 12
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs
  35. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  36. 8
      tests/Avalonia.LeakTests/BindingExpressionTests.cs
  37. 4
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  38. 2
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs
  39. 6
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs
  40. 4
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs
  41. 57
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs
  42. 4
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs
  43. 27
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs
  44. 2
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs

2
src/Avalonia.Base/AvaloniaObject.cs

@ -611,7 +611,7 @@ namespace Avalonia
internal IDisposable Bind( internal IDisposable Bind(
AvaloniaProperty property, AvaloniaProperty property,
BindingExpression expression, UntypedBindingExpressionBase expression,
BindingPriority priority) BindingPriority priority)
{ {
return property.RouteBind(this, expression, priority); return property.RouteBind(this, expression, priority);

2
src/Avalonia.Base/AvaloniaProperty.cs

@ -569,7 +569,7 @@ namespace Avalonia
/// <param name="priority">The priority.</param> /// <param name="priority">The priority.</param>
internal abstract IDisposable RouteBind( internal abstract IDisposable RouteBind(
AvaloniaObject o, AvaloniaObject o,
BindingExpression source, UntypedBindingExpressionBase source,
BindingPriority priority); BindingPriority priority);
/// <summary> /// <summary>

14
src/Avalonia.Base/Data/BindingNotification.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Avalonia.Data namespace Avalonia.Data
{ {
@ -288,4 +289,17 @@ namespace Avalonia.Data
a?.Message == b?.Message; 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,
};
}
}
} }

2
src/Avalonia.Base/Data/BindingOperations.cs

@ -24,7 +24,7 @@ namespace Avalonia.Data
_ = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
_ = binding ?? throw new ArgumentNullException(nameof(binding)); _ = binding ?? throw new ArgumentNullException(nameof(binding));
if (binding.Source is BindingExpression expression) if (binding.Expression is { } expression)
{ {
return target.Bind(property, expression, BindingPriority.LocalValue); return target.Bind(property, expression, BindingPriority.LocalValue);
} }

18
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; }
}

56
src/Avalonia.Base/Data/Core/BindingExpression.Observable.cs

@ -1,56 +0,0 @@
using System;
namespace Avalonia.Data.Core;
// Mostly for unit tests, BindingExpression implements IObservable<object?> and IObserver<object?>.
// We limit support to a single subscriber in this scenario.
internal partial class BindingExpression : IObservable<object?>, IObserver<object?>
{
IDisposable IObservable<object?>.Subscribe(IObserver<object?> 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<object?>.OnCompleted() { }
void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.OnNext(object? value) => SetValue(value);
private class ObservableSink : IBindingExpressionSink
{
private IObserver<object?> _observer;
public ObservableSink(IObserver<object?> 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();
}
}

33
src/Avalonia.Base/Data/Core/BindingExpression.ValueEntry.cs

@ -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();
}

383
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -10,6 +10,7 @@ using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Parsers;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Data.Core; namespace Avalonia.Data.Core;
@ -20,22 +21,13 @@ namespace Avalonia.Data.Core;
/// A <see cref="BindingExpression"/> represents a untyped binding which has been /// A <see cref="BindingExpression"/> represents a untyped binding which has been
/// instantiated on an object. /// instantiated on an object.
/// </remarks> /// </remarks>
internal partial class BindingExpression : IDescription, IDisposable internal partial class BindingExpression : UntypedBindingExpressionBase, IDescription, IDisposable
{ {
internal static readonly WeakReference<object?> NullReference = new(null);
private readonly WeakReference<object?>? _source; private readonly WeakReference<object?>? _source;
private readonly WeakReference<AvaloniaObject?> _target;
private readonly BindingMode _mode; private readonly BindingMode _mode;
private readonly IReadOnlyList<ExpressionNode> _nodes; private readonly IReadOnlyList<ExpressionNode> _nodes;
private readonly TargetTypeConverter? _targetTypeConverter; private readonly TargetTypeConverter? _targetTypeConverter;
private bool _isRunning; private readonly UncommonFields? _uncommon;
private BindingPriority _priority;
private bool _produceValue;
private IBindingExpressionSink? _sink;
private AvaloniaProperty? _targetProperty;
private WeakReference<object?>? _value;
private BindingError? _error;
private UncommonFields? _uncommon;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BindingExpression"/> class. /// Initializes a new instance of the <see cref="BindingExpression"/> class.
@ -53,9 +45,7 @@ internal partial class BindingExpression : IDescription, IDisposable
/// </param> /// </param>
/// <param name="mode">The binding mode.</param> /// <param name="mode">The binding mode.</param>
/// <param name="stringFormat">The format string to use.</param> /// <param name="stringFormat">The format string to use.</param>
/// <param name="target">The target to which the value will be written.</param>
/// <param name="targetNullValue">The null target value.</param> /// <param name="targetNullValue">The null target value.</param>
/// <param name="targetProperty">The target property.</param>
/// <param name="targetTypeConverter"> /// <param name="targetTypeConverter">
/// A final type converter to be run on the produced value. /// A final type converter to be run on the produced value.
/// </param> /// </param>
@ -69,28 +59,20 @@ internal partial class BindingExpression : IDescription, IDisposable
bool enableDataValidation = false, bool enableDataValidation = false,
BindingMode mode = BindingMode.OneWay, BindingMode mode = BindingMode.OneWay,
string? stringFormat = null, string? stringFormat = null,
AvaloniaObject? target = null,
object? targetNullValue = null, object? targetNullValue = null,
AvaloniaProperty? targetProperty = null,
TargetTypeConverter? targetTypeConverter = null) TargetTypeConverter? targetTypeConverter = null)
: base(enableDataValidation)
{ {
if (mode == BindingMode.Default) if (mode == BindingMode.Default)
throw new ArgumentException("Binding mode cannot be Default.", nameof(mode)); 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) if (source == AvaloniaProperty.UnsetValue)
source = null; source = null;
_source = new(source); _source = new(source);
_target = new(target);
_targetProperty = targetProperty;
_mode = mode; _mode = mode;
_nodes = nodes; _nodes = nodes;
_targetTypeConverter = targetTypeConverter; _targetTypeConverter = targetTypeConverter;
IsDataValidationEnabled = enableDataValidation;
if (converter is not null || if (converter is not null ||
converterCulture 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 Type? SourceType => (LeafNode as ISettableNode)?.ValueType;
public AvaloniaProperty? TargetProperty => _targetProperty;
public Type TargetType => _targetProperty?.PropertyType ?? typeof(object);
public IValueConverter? Converter => _uncommon?._converter; public IValueConverter? Converter => _uncommon?._converter;
public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture; public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture;
public object? ConverterParameter => _uncommon?._converterParameter; public object? ConverterParameter => _uncommon?._converterParameter;
public object? FallbackValue => _uncommon is not null ? _uncommon._fallbackValue : AvaloniaProperty.UnsetValue; 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 object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue;
public ExpressionNode LeafNode => _nodes[_nodes.Count - 1]; public ExpressionNode LeafNode => _nodes[_nodes.Count - 1];
public string? StringFormat => _uncommon?._stringFormat; public string? StringFormat => _uncommon?._stringFormat;
/// <summary>
/// Gets the current value of the binding expression.
/// </summary>
/// <returns>
/// The current value or <see cref="AvaloniaProperty.UnsetValue"/> if the binding was unable
/// to read a value.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The binding expression has not been started.
/// </exception>
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;
}
/// <summary>
/// Gets the current value of the binding expression or the default value for the target property.
/// </summary>
/// <returns>
/// The current value or the target property default.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The binding expression has not been started.
/// </exception>
public object? GetValueOrDefault()
{
var result = GetValue();
if (result == AvaloniaProperty.UnsetValue)
result = GetCachedDefaultValue();
return result;
}
/// <summary>
/// Gets the data validation state, if supported.
/// </summary>
/// <param name="state">The binding error state.</param>
/// <param name="error">The current binding error, if any.</param>
/// <returns>
/// True if the expression supports data validation, otherwise false.
/// </returns>
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;
}
}
/// <summary> /// <summary>
/// Writes the specified value to the binding source if possible. /// Writes the specified value to the binding source if possible.
/// </summary> /// </summary>
@ -223,7 +137,7 @@ internal partial class BindingExpression : IDescription, IDisposable
/// <returns> /// <returns>
/// True if the value could be written to the binding source; otherwise false. /// True if the value could be written to the binding source; otherwise false.
/// </returns> /// </returns>
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) if (_nodes.Count == 0 || LeafNode is not ISettableNode setter || setter.ValueType is not { } type)
return false; return false;
@ -261,7 +175,7 @@ internal partial class BindingExpression : IDescription, IDisposable
} }
// Don't set the value if it's unchanged. // 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; return true;
try try
@ -274,61 +188,6 @@ internal partial class BindingExpression : IDescription, IDisposable
} }
} }
/// <summary>
/// Initializes the binding expression with the specified subscriber and target property but
/// does not start it.
/// </summary>
/// <param name="subscriber">The subscriber.</param>
/// <param name="targetProperty">The target property.</param>
/// <param name="priority">The priority of the binding.</param>
/// <exception cref="AvaloniaInternalException">
/// <paramref name="targetProperty"/> is different to that passed in the constructor, if one was
/// passed there.
/// </exception>
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;
}
/// <summary>
/// Starts the binding expression with the specified subscriber and target property..
/// </summary>
/// <param name="subscriber">The subscriber.</param>
/// <param name="targetProperty">The target property.</param>
/// <param name="priority">The priority of the binding.</param>
public void Start(
IBindingExpressionSink subscriber,
AvaloniaProperty targetProperty,
BindingPriority priority)
{
Initialize(subscriber, targetProperty, priority);
Start(produceValue: true);
}
/// <summary>
/// Terminates the binding.
/// </summary>
public void Dispose()
{
if (_sink is null)
return;
Stop();
var sink = _sink;
_sink = null;
sink.OnCompleted(this);
}
/// <summary> /// <summary>
/// Creates an <see cref="BindingExpression"/> from an expression tree. /// Creates an <see cref="BindingExpression"/> from an expression tree.
/// </summary> /// </summary>
@ -342,9 +201,7 @@ internal partial class BindingExpression : IDescription, IDisposable
/// <param name="enableDataValidation">Whether data validation should be enabled for the binding.</param> /// <param name="enableDataValidation">Whether data validation should be enabled for the binding.</param>
/// <param name="fallbackValue">The fallback value.</param> /// <param name="fallbackValue">The fallback value.</param>
/// <param name="mode">The binding mode.</param> /// <param name="mode">The binding mode.</param>
/// <param name="target">The target to which the value will be written.</param>
/// <param name="targetNullValue">The null target value.</param> /// <param name="targetNullValue">The null target value.</param>
/// <param name="targetProperty">The target property.</param>
/// <param name="allowReflection">Whether to allow reflection for target type conversion.</param> /// <param name="allowReflection">Whether to allow reflection for target type conversion.</param>
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal static BindingExpression Create<TIn, TOut>( internal static BindingExpression Create<TIn, TOut>(
@ -356,9 +213,7 @@ internal partial class BindingExpression : IDescription, IDisposable
bool enableDataValidation = false, bool enableDataValidation = false,
Optional<object?> fallbackValue = default, Optional<object?> fallbackValue = default,
BindingMode mode = BindingMode.OneWay, BindingMode mode = BindingMode.OneWay,
AvaloniaObject? target = null,
object? targetNullValue = null, object? targetNullValue = null,
AvaloniaProperty? targetProperty = null,
bool allowReflection = true) bool allowReflection = true)
where TIn : class? where TIn : class?
{ {
@ -374,9 +229,7 @@ internal partial class BindingExpression : IDescription, IDisposable
converterParameter: converterParameter, converterParameter: converterParameter,
enableDataValidation: enableDataValidation, enableDataValidation: enableDataValidation,
mode: mode, mode: mode,
target: target,
targetNullValue: targetNullValue, targetNullValue: targetNullValue,
targetProperty: targetProperty,
targetTypeConverter: allowReflection ? targetTypeConverter: allowReflection ?
TargetTypeConverter.GetReflectionConverter() : TargetTypeConverter.GetReflectionConverter() :
TargetTypeConverter.GetDefaultConverter()); 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 // The leaf node has changed. If the binding mode is not OneWayToSource, publish the
// value to the target. // value to the target.
if (_mode != BindingMode.OneWayToSource) 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 the binding mode is OneTime, then stop the binding.
if (_mode == BindingMode.OneTime) if (_mode == BindingMode.OneTime)
@ -448,21 +306,17 @@ internal partial class BindingExpression : IDescription, IDisposable
if (ShouldLogError(out var target)) if (ShouldLogError(out var target))
Log(target, error, errorPoint); Log(target, error, errorPoint);
// Clear the current value. // Clear the current value and publish the error.
UpdateValue(AvaloniaProperty.UnsetValue, null, out var hasValueChanged, out _); var bindingError = new BindingError(
// And store the error.
_error = new(
new BindingChainException(error, Description, errorPoint.ToString()), new BindingChainException(error, Description, errorPoint.ToString()),
BindingValueType.BindingError); BindingErrorType.Error);
ConvertAndPublishValue(AvaloniaProperty.UnsetValue, bindingError);
PublishValue(hasValueChanged, hasErrorChanged: true);
} }
internal void OnDataValidationError(Exception error) internal void OnDataValidationError(Exception error)
{ {
_error = new(error, BindingValueType.DataValidationError); var bindingError = new BindingError(error, BindingErrorType.DataValidationError);
PublishValue(hasValueChanged: false, hasErrorChanged: true); PublishValue(UnchangedValue, bindingError);
} }
private string CalculateErrorPoint(int nodeIndex) private string CalculateErrorPoint(int nodeIndex)
@ -478,27 +332,6 @@ internal partial class BindingExpression : IDescription, IDisposable
return result.ToString(); 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) private void Log(AvaloniaObject target, string error, LogEventLevel level = LogEventLevel.Warning)
{ {
if (!Logger.TryGet(level, LogArea.Binding, out var log)) if (!Logger.TryGet(level, LogArea.Binding, out var log))
@ -507,7 +340,7 @@ internal partial class BindingExpression : IDescription, IDisposable
log.Log( log.Log(
target, target,
"An error occurred binding {Property} to {Expression}: {Message}", "An error occurred binding {Property} to {Expression}: {Message}",
(object?)_targetProperty ?? "(unknown)", (object?)TargetProperty ?? "(unknown)",
Description, Description,
error); error);
} }
@ -520,7 +353,7 @@ internal partial class BindingExpression : IDescription, IDisposable
log.Log( log.Log(
target, target,
"An error occurred binding {Property} to {Expression} at {ExpressionErrorPoint}: {Message}", "An error occurred binding {Property} to {Expression} at {ExpressionErrorPoint}: {Message}",
(object?)_targetProperty ?? "(unknown)", (object?)TargetProperty ?? "(unknown)",
Description, Description,
errorPoint, errorPoint,
error); error);
@ -528,42 +361,28 @@ internal partial class BindingExpression : IDescription, IDisposable
private bool ShouldLogError([NotNullWhen(true)] out AvaloniaObject? target) private bool ShouldLogError([NotNullWhen(true)] out AvaloniaObject? target)
{ {
if (!_target.TryGetTarget(out target)) if (!TryGetTarget(out target))
return false; return false;
if (_nodes.Count > 0 && _nodes[0] is SourceNode sourceNode) if (_nodes.Count > 0 && _nodes[0] is SourceNode sourceNode)
return sourceNode.ShouldLogErrors(target); return sourceNode.ShouldLogErrors(target);
return true; 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 (_source?.TryGetTarget(out var source) == true)
{ {
if (_nodes.Count > 0) if (_nodes.Count > 0)
{
_nodes[0].SetSource(source, null); _nodes[0].SetSource(source, null);
}
else else
{ PublishValue(source);
_value = new(source);
_error = null;
PublishValue(hasValueChanged: true, hasErrorChanged: false);
}
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource &&
_target.TryGetTarget(out var target) && TryGetTarget(out var target) &&
_targetProperty is not null) TargetProperty is not null)
{ {
if (_mode is BindingMode.OneWayToSource) if (_mode is BindingMode.OneWayToSource)
SetValue(target.GetValue(_targetProperty)); PublishValue(target.GetValue(TargetProperty));
target.PropertyChanged += OnTargetPropertyChanged; target.PropertyChanged += OnTargetPropertyChanged;
} }
@ -572,58 +391,31 @@ internal partial class BindingExpression : IDescription, IDisposable
{ {
OnNodeError(-1, "Binding Source is null."); OnNodeError(-1, "Binding Source is null.");
} }
_produceValue = true;
} }
private void Stop() protected override void StopCore()
{ {
foreach (var node in _nodes) foreach (var node in _nodes)
node.Reset(); node.Reset();
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource &&
_target.TryGetTarget(out var target)) TryGetTarget(out var target))
{ {
target.PropertyChanged -= OnTargetPropertyChanged; target.PropertyChanged -= OnTargetPropertyChanged;
} }
_isRunning = false;
_value = null;
} }
private void UpdateValue( private void ConvertAndPublishValue(object? value, BindingError? error)
object? value,
Exception? dataValidationError,
out bool hasValueChanged,
out bool hasErrorChanged)
{ {
var isTargetNullValue = false; var isTargetNullValue = false;
var hadError = _error is not null;
// All values other than DoNothing should be passed to the converter. // All values other than DoNothing should be passed to the converter.
if (value != BindingOperations.DoNothing && Converter is { } 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. // Check this here as the converter may return DoNothing.
if (value == BindingOperations.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; return;
}
// TargetNullValue only applies when the value is null: UnsetValue indicates that there // 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. // 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) else if (_targetTypeConverter is not null)
{ {
// Otherwise, if we have a target type converter, convert the value to the target type. // 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) if (value == AvaloniaProperty.UnsetValue && FallbackValue != AvaloniaProperty.UnsetValue)
value = ConvertFallback(FallbackValue, nameof(FallbackValue)); value = ConvertFallback(FallbackValue, nameof(FallbackValue));
// Update the stored value. // Publish the value.
var oldValue = _value; PublishValue(value, error);
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);
} }
private void WriteTargetValueToSource() private void WriteTargetValueToSource()
{ {
if (_mode != BindingMode.OneWayToSource) Debug.Assert(_mode == BindingMode.OneWayToSource);
return;
if (_target.TryGetTarget(out var target) && if (TryGetTarget(out var target) &&
_targetProperty is not null && TargetProperty is not null &&
target.GetValue(_targetProperty) is var value && target.GetValue(TargetProperty) is var value &&
!Equals(value, LeafNode.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) 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, IValueConverter converter,
object? converterParameter, object? converterParameter,
object? value, object? value,
Type targetType) Type targetType,
ref BindingError? error)
{ {
try try
{ {
@ -750,7 +495,7 @@ internal partial class BindingExpression : IDescription, IDisposable
if (ShouldLogError(out var target)) if (ShouldLogError(out var target))
Log(target, $"{message}: {e.Message}", LogEventLevel.Warning); 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; return AvaloniaProperty.UnsetValue;
} }
} }
@ -763,53 +508,31 @@ internal partial class BindingExpression : IDescription, IDisposable
if (_targetTypeConverter.TryConvert(fallback, TargetType, ConverterCulture, out var result)) if (_targetTypeConverter.TryConvert(fallback, TargetType, ConverterCulture, out var result))
return 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); Log(target, $"Could not convert {fallbackName} '{fallback}' to '{TargetType}'.", LogEventLevel.Error);
return AvaloniaProperty.UnsetValue; 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; 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; return result;
var valueString = value?.ToString() ?? "(null)"; var valueString = value?.ToString() ?? "(null)";
var valueTypeName = value?.GetType().FullName ?? "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)) if (ShouldLogError(out var target))
Log(target, message, LogEventLevel.Warning); Log(target, message, LogEventLevel.Warning);
_error = new(new InvalidCastException(message), BindingValueType.BindingError); error = new(new InvalidCastException(message), BindingErrorType.Error);
return AvaloniaProperty.UnsetValue; 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 private class UncommonFields
{ {
public IValueConverter? _converter; public IValueConverter? _converter;
@ -818,7 +541,5 @@ internal partial class BindingExpression : IDescription, IDisposable
public object? _fallbackValue; public object? _fallbackValue;
public string? _stringFormat; public string? _stringFormat;
public object? _targetNullValue; public object? _targetNullValue;
public object? _defaultValue;
public bool _isDefaultValueInitialized;
} }
} }

10
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()
{
}
}

19
src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs

@ -59,7 +59,7 @@ internal abstract class ExpressionNode
{ {
get get
{ {
return _value == BindingExpression.NullReference || return _value == UntypedBindingExpressionBase._nullReference ||
_value?.TryGetTarget(out _) == true; _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 // 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 // OnSourceChanged as we don't want to raise errors for subsequent nodes in the
// binding change. // binding change.
_value = BindingExpression.NullReference; _value = BindingExpression._nullReference;
} }
else if (source != oldSource) else if (source != oldSource)
{ {
@ -199,11 +199,20 @@ internal abstract class ExpressionNode
if (valueOrNotification is BindingNotification notification) if (valueOrNotification is BindingNotification notification)
{ {
if (notification.ErrorType == BindingErrorType.Error) if (notification.ErrorType == BindingErrorType.Error)
{
SetError(notification.Error!); SetError(notification.Error!);
}
else if (notification.ErrorType == BindingErrorType.DataValidationError) else if (notification.ErrorType == BindingErrorType.DataValidationError)
SetValue(notification.Value, notification.Error); {
if (notification.HasValue)
SetValue(notification.Value, notification.Error);
else
SetDataValidationError(notification.Error!);
}
else else
{
SetValue(notification.Value, null); SetValue(notification.Value, null);
}
} }
else else
{ {
@ -237,11 +246,11 @@ internal abstract class ExpressionNode
// - The new value is different to the old value // - The new value is different to the old value
if (_value is null || if (_value is null ||
dataValidationError is not null || dataValidationError is not null ||
(dataValidationError is null && Owner.HasDataValidationError) || (dataValidationError is null && Owner.ErrorType == BindingErrorType.DataValidationError) ||
_value.TryGetTarget(out var oldValue) == false || _value.TryGetTarget(out var oldValue) == false ||
!Equals(oldValue, value)) !Equals(oldValue, value))
{ {
_value = value is null ? BindingExpression.NullReference : new(value); _value = value is null ? BindingExpression._nullReference : new(value);
Owner.OnNodeValueChanged(Index, value, dataValidationError); Owner.OnNodeValueChanged(Index, value, dataValidationError);
} }
} }

24
src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs

@ -3,18 +3,32 @@
internal interface IBindingExpressionSink internal interface IBindingExpressionSink
{ {
/// <summary> /// <summary>
/// Called when a <see cref="BindingExpression"/>'s value or error state /// Called when an <see cref="UntypedBindingExpressionBase"/>'s value or error state
/// changes. /// changes.
/// </summary> /// </summary>
/// <param name="instance">The binding expression.</param> /// <param name="instance">The binding expression.</param>
/// <param name="hasValueChanged">
/// Indicates whether <paramref name="value"/> represents a new value produced by the binding.
/// </param>
/// <param name="hasErrorChanged">
/// Indicates whether <paramref name="error"/> represents a new error produced by the binding.
/// </param>
/// <param name="value">
/// The new binding value; if <paramref name="hasValueChanged"/> is true.
/// </param>
/// <param name="error">
/// The new binding error; if <paramref name="hasErrorChanged"/> is true.
/// </param>
void OnChanged( void OnChanged(
BindingExpression instance, UntypedBindingExpressionBase instance,
bool hasValueChanged, bool hasValueChanged,
bool hasErrorChanged); bool hasErrorChanged,
object? value,
BindingError? error);
/// <summary> /// <summary>
/// Called when a <see cref="BindingExpression"/> completes. /// Called when an <see cref="UntypedBindingExpressionBase"/> completes.
/// </summary> /// </summary>
/// <param name="instance">The binding expression.</param> /// <param name="instance">The binding expression.</param>
void OnCompleted(BindingExpression instance); void OnCompleted(UntypedBindingExpressionBase instance);
} }

455
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;
/// <summary>
/// Base class for binding expressions which produce untyped values.
/// </summary>
internal abstract class UntypedBindingExpressionBase : BindingExpressionBase, IDisposable, IValueEntry
{
protected static readonly object UnchangedValue = new();
internal static readonly WeakReference<object?> _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<AvaloniaObject?>? _target;
private WeakReference<object?>? _value;
/// <summary>
/// Initializes a new instance of the <see cref="UntypedBindingExpressionBase"/> class.
/// </summary>
/// <param name="isDataValidationEnabled">Whether data validation is enabled.</param>
public UntypedBindingExpressionBase(bool isDataValidationEnabled)
{
_isDataValidationEnabled = isDataValidationEnabled;
}
/// <summary>
/// Gets the current error state of the binding expression.
/// </summary>
public BindingErrorType ErrorType => _error?.ErrorType ?? BindingErrorType.None;
/// <summary>
/// Gets a value indicating whether data validation is enabled for the binding expression.
/// </summary>
public bool IsDataValidationEnabled => _isDataValidationEnabled;
/// <summary>
/// Gets a value indicating whether the binding expression is currently running.
/// </summary>
public bool IsRunning => _isRunning;
/// <summary>
/// Gets the priority of the binding expression.
/// </summary>
public BindingPriority Priority { get; private set; }
/// <summary>
/// Gets the <see cref="AvaloniaProperty"/> which the binding expression is targeting.
/// </summary>
public AvaloniaProperty? TargetProperty { get; private set; }
/// <summary>
/// Gets the target type of the binding expression; that is, the type that values produced by
/// the expression should be converted to.
/// </summary>
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();
/// <summary>
/// Produces an observable which can be used to observe the value of the binding expression.
/// </summary>
/// <returns>An observable subject.</returns>
/// <exception cref="InvalidOperationException">
/// The binding expression is already instantiated on an AvaloniaObject.
/// </exception>
/// <remarks>
/// This method is mostly here for backwards compatibility with <see cref="InstancedBinding"/>
/// 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 <see cref="IValueEntry"/> instantiated in a
/// <see cref="ValueStore"/>. 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 <see cref="AvaloniaObject"/>.
///
/// A binding expression may only act as an observable or as a binding expression targeting an
/// AvaloniaObject, not both.
/// </remarks>
public IAvaloniaSubject<object?> 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;
}
/// <summary>
/// Produces an observable which can be used to observe the value of the binding expression
/// for unit testing.
/// </summary>
/// <param name="targetType">The <see cref="TargetType"/>.</param>
/// <returns>An observable subject.</returns>
/// <exception cref="InvalidOperationException">
/// The binding expression is already instantiated on an AvaloniaObject.
/// </exception>
/// <remarks>
/// This method should be considered obsolete and new unit tests should not be written to use
/// it. For more information see <see cref="ToObservable()"/>.
/// </remarks>
public IAvaloniaSubject<object?> ToObservable(Type targetType)
{
var o = ToObservable();
TargetType = targetType;
return o;
}
/// <summary>
/// Terminates the binding.
/// </summary>
public virtual void Dispose()
{
if (_sink is null)
return;
Stop();
var sink = _sink;
_sink = null;
sink.OnCompleted(this);
}
/// <summary>
/// Gets the current value of the binding expression.
/// </summary>
/// <returns>
/// The current value or <see cref="AvaloniaProperty.UnsetValue"/> if the binding was unable
/// to read a value.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The binding expression has not been started.
/// </exception>
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;
}
/// <summary>
/// Gets the current value of the binding expression or the default value for the target property.
/// </summary>
/// <returns>
/// The current value or the target property default.
/// </returns>
public object? GetValueOrDefault()
{
var result = GetValue();
if (result == AvaloniaProperty.UnsetValue)
result = GetCachedDefaultValue();
return result;
}
/// <summary>
/// Attaches the binding expression to a subscriber with the specified subscriber but does not
/// start it.
/// </summary>
/// <param name="sink">The subscriber.</param>
/// <param name="target">The target object.</param>
/// <param name="targetProperty">The target property.</param>
/// <param name="priority">The priority of the binding.</param>
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;
}
/// <summary>
/// Initializes the binding expression with the specified subscriber and target property and
/// starts it.
/// </summary>
/// <param name="subscriber">The subscriber.</param>
/// <param name="target">The target object.</param>
/// <param name="targetProperty">The target property.</param>
/// <param name="priority">The priority of the binding.</param>
public void Start(
IBindingExpressionSink subscriber,
AvaloniaObject target,
AvaloniaProperty targetProperty,
BindingPriority priority)
{
Attach(subscriber, target, targetProperty, priority);
Start(produceValue: true);
}
/// <summary>
/// When overridden in a derived class, writes the specified value to the binding source if
/// possible.
/// </summary>
/// <param name="value">The value to write.</param>
/// <returns>
/// True if the value could be written to the binding source; otherwise false.
/// </returns>
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();
/// <summary>
/// When overridden in a derived class, starts the binding expression.
/// </summary>
/// <remarks>
/// This method should not be called directly; instead call <see cref="Start(bool)"/>.
/// </remarks>
protected abstract void StartCore();
/// <summary>
/// When overridden in a derived class, stops the binding expression.
/// </summary>
/// <remarks>
/// This method should not be called directly; instead call <see cref="Stop"/>.
/// </remarks>
protected abstract void StopCore();
/// <summary>
/// Publishes a new value and/or error state to the target.
/// </summary>
/// <param name="value">The new value, or <see cref="UnchangedValue"/>.</param>
/// <param name="error">The new binding or data validation error.</param>
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));
}
}
/// <summary>
/// Starts the binding expression by calling <see cref="StartCore"/>.
/// </summary>
/// <param name="produceValue">
/// Indicates whether the binding expression should produce an initial value.
/// </param>
protected void Start(bool produceValue)
{
if (_isRunning)
return;
_isRunning = true;
try
{
_produceValue = produceValue;
StartCore();
}
finally
{
_produceValue = true;
}
}
/// <summary>
/// Stops the binding expression by calling <see cref="StopCore"/>.
/// </summary>
protected void Stop()
{
StopCore();
_isRunning = false;
_value = null;
}
/// <summary>
/// Tries to retrieve the target for the binding expression.
/// </summary>
/// <param name="target">
/// When this method returns, contains the target object, if it is available.
/// </param>
/// <returns>true if the target was retrieved; otherwise, false.</returns>
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<object?>,
IBindingExpressionSink,
IAvaloniaSubject<object?>
{
private readonly UntypedBindingExpressionBase _expression;
private WeakReference<object?>? _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<object?>.OnCompleted() { }
void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.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<object> 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);
}
}
}

17
src/Avalonia.Base/Data/InstancedBinding.cs

@ -1,5 +1,6 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Data.Core;
using Avalonia.Reactive; using Avalonia.Reactive;
using ObservableEx = Avalonia.Reactive.Observable; using ObservableEx = Avalonia.Reactive.Observable;
@ -16,6 +17,9 @@ namespace Avalonia.Data
/// </remarks> /// </remarks>
public sealed class InstancedBinding public sealed class InstancedBinding
{ {
private IObservable<object?>? _observable;
private UntypedBindingExpressionBase? _expression;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="InstancedBinding"/> class. /// Initializes a new instance of the <see cref="InstancedBinding"/> class.
/// </summary> /// </summary>
@ -32,7 +36,14 @@ namespace Avalonia.Data
{ {
Mode = mode; Mode = mode;
Priority = priority; 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));
} }
/// <summary> /// <summary>
@ -48,11 +59,13 @@ namespace Avalonia.Data
/// <summary> /// <summary>
/// Gets the binding source observable. /// Gets the binding source observable.
/// </summary> /// </summary>
public IObservable<object?> Source { get; } public IObservable<object?> Source => _observable ??= _expression!.ToObservable();
[Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)]
public IObservable<object?> Observable => Source; public IObservable<object?> Observable => Source;
internal UntypedBindingExpressionBase? Expression => _expression;
/// <summary> /// <summary>
/// Creates a new one-time binding with a fixed value. /// Creates a new one-time binding with a fixed value.
/// </summary> /// </summary>

5
src/Avalonia.Base/DirectPropertyBase.cs

@ -183,7 +183,10 @@ namespace Avalonia
return o.Bind(this, source); 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); return o.GetValueStore().AddBinding(this, source);
} }

4
src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs

@ -15,9 +15,7 @@ namespace Avalonia.PropertyStore
{ {
} }
public IValueEntry AddBinding<T>( public IValueEntry AddBinding<T>(UntypedBindingExpressionBase source)
StyledProperty<T> property,
BindingExpression source)
{ {
Add(source); Add(source);
return source; return source;

27
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -45,7 +45,7 @@ namespace Avalonia.PropertyStore
public IDisposable AddBinding<T>( public IDisposable AddBinding<T>(
StyledProperty<T> property, StyledProperty<T> property,
BindingExpression source, UntypedBindingExpressionBase source,
BindingPriority priority) BindingPriority priority)
{ {
if (priority == BindingPriority.LocalValue) if (priority == BindingPriority.LocalValue)
@ -53,7 +53,7 @@ namespace Avalonia.PropertyStore
DisposeExistingLocalValueBinding(property); DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new(); _localValueBindings ??= new();
_localValueBindings[property.Id] = source; _localValueBindings[property.Id] = source;
source.Start(this, property, priority); source.Start(this, Owner, property, priority);
return source; return source;
} }
else else
@ -61,10 +61,10 @@ namespace Avalonia.PropertyStore
var effective = GetEffectiveValue(property); var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _); var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
frame.AddBinding(property, source); frame.AddBinding<T>(source);
if (effective is null || priority <= effective.Priority) if (effective is null || priority <= effective.Priority)
source.Start(this, property, priority); source.Start(this, Owner, property, priority);
return source; return source;
} }
@ -154,12 +154,12 @@ namespace Avalonia.PropertyStore
} }
} }
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, BindingExpression source) public IDisposable AddBinding<T>(DirectPropertyBase<T> property, UntypedBindingExpressionBase source)
{ {
DisposeExistingLocalValueBinding(property); DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new(); _localValueBindings ??= new();
_localValueBindings[property.Id] = source; _localValueBindings[property.Id] = source;
source.Start(this, property, BindingPriority.LocalValue); source.Start(this, Owner, property, BindingPriority.LocalValue);
return source; return source;
} }
@ -744,15 +744,16 @@ namespace Avalonia.PropertyStore
} }
void IBindingExpressionSink.OnChanged( void IBindingExpressionSink.OnChanged(
BindingExpression instance, UntypedBindingExpressionBase instance,
bool hasValueChanged, bool hasValueChanged,
bool hasErrorChanged) bool hasErrorChanged,
object? value,
BindingError? error)
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
Debug.Assert(instance.TargetProperty is not null); Debug.Assert(instance.TargetProperty is not null);
var property = instance.TargetProperty; var property = instance.TargetProperty;
var value = instance.GetValueOrDefault();
if (property.IsDirect) 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); var e = error?.ErrorType.ToBindingValueType() ?? BindingValueType.Value;
Owner.OnUpdateDataValidation(property, state, error); Owner.OnUpdateDataValidation(property, e, error?.Exception);
} }
} }
@ -800,7 +801,7 @@ namespace Avalonia.PropertyStore
/// Called by a binding expression when the binding produces completes. /// Called by a binding expression when the binding produces completes.
/// </summary> /// </summary>
/// <param name="instance">The binding expression.</param> /// <param name="instance">The binding expression.</param>
void IBindingExpressionSink.OnCompleted(BindingExpression instance) void IBindingExpressionSink.OnCompleted(UntypedBindingExpressionBase instance)
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
Debug.Assert(instance.TargetProperty is not null); Debug.Assert(instance.TargetProperty is not null);

5
src/Avalonia.Base/StyledProperty.cs

@ -228,7 +228,10 @@ namespace Avalonia
return target.Bind<TValue>(this, source, priority); return target.Bind<TValue>(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); return o.GetValueStore().AddBinding(this, source, priority);
} }

2
src/Avalonia.Base/Styling/Setter.cs

@ -120,7 +120,7 @@ namespace Avalonia.Styling
{ {
if (i.Source is BindingExpression expression) if (i.Source is BindingExpression expression)
{ {
expression.Initialize(target.GetValueStore(), Property, instance.Priority); expression.Attach(target.GetValueStore(), target, Property, instance.Priority);
return expression; return expression;
} }
else else

20
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -429,5 +429,25 @@ namespace Avalonia.Utilities
return null; return null;
} }
/// <summary>
/// Determines whether the specified object instances are "identity" equal which means
/// reference equal for reference types and <see cref="object.Equals(object?)"/> for value
/// types.
/// </summary>
/// <param name="a">The first object to compare.</param>
/// <param name="b">The second object to compare.</param>
/// <param name="type">
/// The type which determines whether the objects should be treated as a reference or
/// value type.
/// </param>
/// <returns>True if the objects are considered equal; otherwise false.</returns>
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);
}
} }
} }

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs

@ -71,9 +71,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
enableDataValidation: enableDataValidation, enableDataValidation: enableDataValidation,
mode: ResolveBindingMode(target, targetProperty), mode: ResolveBindingMode(target, targetProperty),
stringFormat: StringFormat, stringFormat: StringFormat,
target: target,
targetNullValue: TargetNullValue, targetNullValue: TargetNullValue,
targetProperty: targetProperty,
targetTypeConverter: TargetTypeConverter.GetDefaultConverter()); targetTypeConverter: TargetTypeConverter.GetDefaultConverter());
return new InstancedBinding(expression, Mode, Priority); return new InstancedBinding(expression, Mode, Priority);

4
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@ -39,14 +39,14 @@ namespace Avalonia.Markup.Xaml.Templates
{ {
if (ItemsSource != null) if (ItemsSource != null)
{ {
var obs = ItemsSource switch var expression = ItemsSource switch
{ {
Binding reflection => reflection.CreateObservableForTreeDataTemplate(item), Binding reflection => reflection.CreateObservableForTreeDataTemplate(item),
CompiledBindingExtension compiled => compiled.CreateObservableForTreeDataTemplate(item), CompiledBindingExtension compiled => compiled.CreateObservableForTreeDataTemplate(item),
_ => throw new InvalidOperationException("TreeDataTemplate currently only supports Binding and CompiledBindingExtension!") _ => 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; return null;

2
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -105,9 +105,7 @@ namespace Avalonia.Data
enableDataValidation: enableDataValidation, enableDataValidation: enableDataValidation,
mode: ResolveBindingMode(target, targetProperty), mode: ResolveBindingMode(target, targetProperty),
stringFormat: StringFormat, stringFormat: StringFormat,
target: target,
targetNullValue: TargetNullValue, targetNullValue: TargetNullValue,
targetProperty: targetProperty,
targetTypeConverter: TargetTypeConverter.GetReflectionConverter()); targetTypeConverter: TargetTypeConverter.GetReflectionConverter());
return new InstancedBinding(expression, Mode, Priority); return new InstancedBinding(expression, Mode, Priority);
} }

5
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@ -175,7 +175,10 @@ namespace Avalonia.Base.UnitTests
throw new NotImplementedException(); 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(); throw new NotImplementedException();
} }

77
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@ -15,16 +15,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
[InvariantCulture] [InvariantCulture]
public class BindingExpressionTests public class BindingExpressionTests
{ {
private static AvaloniaProperty TargetTypeDouble => TargetProperties.DoubleProperty;
private static AvaloniaProperty TargetTypeInt => TargetProperties.IntProperty;
private static AvaloniaProperty TargetTypeString => TargetProperties.StringProperty;
[Fact] [Fact]
public async Task Should_Get_Source_Value() public async Task Should_Get_Source_Value()
{ {
var data = "foo"; var data = "foo";
var target = BindingExpression.Create(data, o => o); var target = BindingExpression.Create(data, o => o).ToObservable();
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -36,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Convert_String_To_Double() public async Task Should_Convert_String_To_Double()
{ {
var data = new Class1 { StringValue = $"{5.6}" }; 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); var result = await target.Take(1);
Assert.Equal(5.6, result); 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() public async Task Getting_Invalid_Double_String_Should_Return_BindingError()
{ {
var data = new Class1 { StringValue = "foo" }; 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); var result = await target.Take(1);
Assert.IsType<BindingNotification>(result); Assert.IsType<BindingNotification>(result);
@ -60,11 +55,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Convert_Set_String_To_Double() public void Should_Convert_Set_String_To_Double()
{ {
var data = new Class1 { StringValue = $"{5.6}" }; 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); Assert.Equal($"{6.7}", data.StringValue);
@ -76,7 +71,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Convert_Double_To_String() public async Task Should_Convert_Double_To_String()
{ {
var data = new Class1 { DoubleValue = 5.6 }; 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); var result = await target.Take(1);
Assert.Equal($"{5.6}", result); Assert.Equal($"{5.6}", result);
@ -90,9 +85,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
var data = new Class1 { DoubleValue = 5.6 }; var data = new Class1 { DoubleValue = 5.6 };
var target = BindingExpression.Create(data, o => o.DoubleValue); 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); Assert.Equal(6.7, data.DoubleValue);
@ -101,14 +96,13 @@ namespace Avalonia.Base.UnitTests.Data.Core
} }
[Fact] [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 data = new Class1 { StringValue = "foo" };
var target = BindingExpression.Create( var target = BindingExpression.Create(
data, data,
o => o.StringValue, o => o.StringValue,
fallbackValue: 42, fallbackValue: 42).ToObservable(typeof(int));
targetProperty: TargetTypeInt);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
@ -122,15 +116,14 @@ namespace Avalonia.Base.UnitTests.Data.Core
} }
[Fact] [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 data = new Class1 { StringValue = "foo" };
var target = BindingExpression.Create( var target = BindingExpression.Create(
data, data,
o => o.StringValue, o => o.StringValue,
enableDataValidation: true, enableDataValidation: true,
fallbackValue: 42, fallbackValue: 42).ToObservable(typeof(int));
targetProperty: TargetTypeInt);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
@ -147,9 +140,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Setting_Invalid_Double_String_Should_Not_Change_Target() public void Setting_Invalid_Double_String_Should_Not_Change_Target()
{ {
var data = new Class1 { DoubleValue = 5.6 }; 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); Assert.Equal(5.6, data.DoubleValue);
@ -163,11 +157,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create( var target = BindingExpression.Create(
data, data,
o => o.DoubleValue, o => o.DoubleValue,
fallbackValue: "9.8", fallbackValue: "9.8");
targetProperty: TargetTypeString);
using (target.Subscribe(_ => { })) using (target.ToObservable().Subscribe(_ => { }))
target.SetValue("foo"); target.WriteValueToSource("foo");
Assert.Equal(9.8, data.DoubleValue); 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() public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
{ {
var data = new Class1 { DoubleValue = 5.6 }; 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(_ => { })) using (target.ToObservable().Subscribe(_ => { }))
target.SetValue(AvaloniaProperty.UnsetValue); target.WriteValueToSource(AvaloniaProperty.UnsetValue);
Assert.Equal(0, data.DoubleValue); Assert.Equal(0, data.DoubleValue);
@ -198,8 +191,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
data, data,
o => o.DoubleValue, o => o.DoubleValue,
converter: converter.Object, converter: converter.Object,
converterParameter: "foo", converterParameter: "foo").ToObservable(typeof(string));
targetProperty: TargetTypeString);
target.Subscribe(_ => { }); target.Subscribe(_ => { });
@ -217,11 +209,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
data, data,
o => o.DoubleValue, o => o.DoubleValue,
converter: converter.Object, converter: converter.Object,
converterParameter: "foo", converterParameter: "foo");
targetProperty: TargetTypeString);
using (target.Subscribe(_ => { })) using (target.ToObservable().Subscribe(_ => { }))
target.SetValue("bar"); target.WriteValueToSource("bar");
converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentCulture)); 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( var target = BindingExpression.Create(
data, data,
o => o.DoubleValue, o => o.DoubleValue,
enableDataValidation: true, enableDataValidation: true);
targetProperty: TargetTypeString);
var result = new List<object>(); var result = new List<object>();
target.Subscribe(x => result.Add(x)); target.ToObservable(typeof(string)).Subscribe(x => result.Add(x));
target.SetValue(1.2); target.WriteValueToSource(1.2);
target.SetValue($"{3.4}"); target.WriteValueToSource($"{3.4}");
target.SetValue("bar"); target.WriteValueToSource("bar");
Assert.Equal( Assert.Equal(
new[] new[]
@ -268,11 +258,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create( var target = BindingExpression.Create(
data, data,
o => o.StringValue, o => o.StringValue,
targetNullValue: "bar", targetNullValue: "bar");
targetProperty: TargetTypeString);
object result = null; object result = null;
target.Subscribe(x => result = x); target.ToObservable().Subscribe(x => result = x);
Assert.Equal("foo", result); Assert.Equal("foo", result);

10
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() public async Task Should_Get_Attached_Property_Value()
{ {
var data = new Class1(); 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); var result = await target.Take(1);
Assert.Equal("foo", result); 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); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Simple_Attached_Value() public void Should_Track_Simple_Attached_Value()
{ {
var data = new Class1(); 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<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); 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<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -93,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
}; };
var result = run(); var result = run();
result.Item1.Subscribe(x => { }); result.Item1.ToObservable().Subscribe(x => { });
GC.Collect(); GC.Collect();

8
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() public async Task Should_Get_Simple_Property_Value()
{ {
var data = new Class1(); 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); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -32,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_ClrProperty_Value() public async Task Should_Get_Simple_ClrProperty_Value()
{ {
var data = new Class1(); 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); var result = await target.Take(1);
Assert.Equal("clr-property", result); Assert.Equal("clr-property", result);
@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Should_Track_Simple_Property_Value() public void Should_Track_Simple_Property_Value()
{ {
var data = new Class1(); 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<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -66,7 +66,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
}; };
var result = run(); var result = run();
result.Item1.Subscribe(x => { }); result.Item1.ToObservable().Subscribe(x => { });
GC.Collect(); GC.Collect();

26
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 target = CreateTarget(data, o => o.MustBePositive, false);
var validationMessageFound = false; var validationMessageFound = false;
target!.OfType<BindingNotification>() target.ToObservable()!.OfType<BindingNotification>()
.Where(x => x.ErrorType == BindingErrorType.DataValidationError) .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
.Subscribe(_ => validationMessageFound = true); .Subscribe(_ => validationMessageFound = true);
target.SetValue(-5); target.WriteValueToSource(-5);
Assert.False(validationMessageFound); Assert.False(validationMessageFound);
@ -41,10 +41,10 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = CreateTarget(data, o => o.MustBePositive); var target = CreateTarget(data, o => o.MustBePositive);
var validationMessageFound = false; var validationMessageFound = false;
target!.OfType<BindingNotification>() target.ToObservable()!.OfType<BindingNotification>()
.Where(x => x.ErrorType == BindingErrorType.DataValidationError) .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
.Subscribe(_ => validationMessageFound = true); .Subscribe(_ => validationMessageFound = true);
target.SetValue(-5); target.WriteValueToSource(-5);
Assert.True(validationMessageFound); Assert.True(validationMessageFound);
@ -55,7 +55,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Indei_Validation_Does_Not_Subscribe_When_DataValidation_Not_Enabled() public void Indei_Validation_Does_Not_Subscribe_When_DataValidation_Not_Enabled()
{ {
var data = new IndeiTest { MustBePositive = 5 }; 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(_ => { }); observer.Subscribe(_ => { });
@ -66,7 +66,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public void Enabled_Indei_Validation_Subscribes() public void Enabled_Indei_Validation_Subscribes()
{ {
var data = new IndeiTest { MustBePositive = 5 }; 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(_ => { }); var sub = observer.Subscribe(_ => { });
Assert.Equal(1, data.ErrorsChangedSubscriptionCount); Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
@ -83,11 +83,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
var observer = CreateTarget(data, o => o.MustBePositive); var observer = CreateTarget(data, o => o.MustBePositive);
var result = new List<object?>(); var result = new List<object?>();
observer.Subscribe(x => result.Add(x)); observer.ToObservable().Subscribe(x => result.Add(x));
observer.SetValue(5); observer.WriteValueToSource(5);
observer.SetValue(-5); observer.WriteValueToSource(-5);
observer.SetValue("foo"); observer.WriteValueToSource("foo");
observer.SetValue(5); observer.WriteValueToSource(5);
Assert.Equal(new[] Assert.Equal(new[]
{ {
@ -120,7 +120,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
Inner = new IndeiTest() Inner = new IndeiTest()
}; };
var observer = CreateTarget(data, o => o.Inner!.MustBePositive); var observer = CreateTarget(data, o => o.Inner!.MustBePositive).ToObservable();
observer.Subscribe(_ => { }); observer.Subscribe(_ => { });
@ -136,7 +136,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var container = new Container(); 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<object?>(); var result = new List<object?>();

30
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_ExpressionTree.cs

@ -15,7 +15,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var target = new object(); 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)); Assert.Equal(target, await observer.Take(1));
GC.KeepAlive(target); GC.KeepAlive(target);
@ -26,7 +26,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var target = new Class1(); 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)); Assert.Null(await observer.Take(1));
@ -46,9 +46,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
var data = new Class1(); var data = new Class1();
var target = BindingExpression.Create(data, o => o.Foo); 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); GC.KeepAlive(data);
@ -59,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new[] { 1, 2, 3, 4 }; 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)); Assert.Equal(data[0], await target.Take(1));
GC.KeepAlive(data); GC.KeepAlive(data);
@ -70,7 +70,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new List<int> { 1, 2, 3, 4 }; var data = new List<int> { 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)); Assert.Equal(data[0], await target.Take(1));
GC.KeepAlive(data); GC.KeepAlive(data);
@ -85,7 +85,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
data.Add(key, new object()); 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)); 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]); 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); GC.KeepAlive(data);
@ -112,7 +112,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
NotifyingBase test = new Class1 { Foo = "Test" }; 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)); Assert.Equal("Test", await target.Take(1));
@ -132,7 +132,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
NotifyingBase test = new Class1 { Foo = "Test" }; 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)); Assert.Equal("Test", await target.Take(1));
@ -144,7 +144,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var test = new Class2(); 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)); Assert.Equal("foo", await target.Take(1));
@ -156,7 +156,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var test = new Class1 { Foo = "Test" }; 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)); Assert.Equal(test.Foo.Length, await target.Take(1));
@ -169,7 +169,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
using (var sync = UnitTestSynchronizationContext.Begin()) using (var sync = UnitTestSynchronizationContext.Begin())
{ {
var data = new { Foo = Task.FromResult("foo") }; 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<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); 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() public async Task Should_Create_Method_Binding()
{ {
var data = new Class3(); 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); var value = await target.Take(1);
Assert.IsAssignableFrom<Delegate>(value); Assert.IsAssignableFrom<Delegate>(value);

44
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() public async Task Should_Get_Array_Value()
{ {
var data = new { Foo = new[] { "foo", "bar" } }; 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); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -39,7 +39,7 @@ public abstract class BindingExpressionTests_Indexer
public async Task Should_Get_MultiDimensional_Array_Value() public async Task Should_Get_MultiDimensional_Array_Value()
{ {
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; 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); var result = await target.Take(1);
Assert.Equal("qux", result); Assert.Equal("qux", result);
@ -51,7 +51,7 @@ public abstract class BindingExpressionTests_Indexer
public async Task Should_Get_Value_For_String_Indexer() public async Task Should_Get_Value_For_String_Indexer()
{ {
var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } }; var data = new { Foo = new Dictionary<string, string> { { "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); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -63,7 +63,7 @@ public abstract class BindingExpressionTests_Indexer
public async Task Should_Get_Value_For_Non_String_Indexer() public async Task Should_Get_Value_For_Non_String_Indexer()
{ {
var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } }; var data = new { Foo = new Dictionary<double, string> { { 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); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -75,7 +75,7 @@ public abstract class BindingExpressionTests_Indexer
public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
{ {
var data = new { Foo = new[] { "foo", "bar" } }; 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); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); 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() public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
{ {
var data = new { Foo = new List<string> { "foo", "bar" } }; var data = new { Foo = new List<string> { "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); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result));
@ -99,7 +99,7 @@ public abstract class BindingExpressionTests_Indexer
public async Task Should_Get_List_Value() public async Task Should_Get_List_Value()
{ {
var data = new { Foo = new List<string> { "foo", "bar" } }; var data = new { Foo = new List<string> { "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); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -111,7 +111,7 @@ public abstract class BindingExpressionTests_Indexer
public void Should_Track_INCC_Add() public void Should_Track_INCC_Add()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = CreateTarget(data, o => o.Foo[2]); var target = CreateTarget(data, o => o.Foo[2]).ToObservable();
var result = new List<object?>(); var result = new List<object?>();
using (var sub = target.Subscribe(x => result.Add(BindingNotification.ExtractValue(x)))) 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() public void Should_Track_INCC_Remove()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = CreateTarget(data, o => o.Foo[0]); var target = CreateTarget(data, o => o.Foo[0]).ToObservable();
var result = new List<object?>(); var result = new List<object?>();
using (var sub = target.Subscribe(result.Add)) using (var sub = target.Subscribe(result.Add))
@ -153,7 +153,7 @@ public abstract class BindingExpressionTests_Indexer
public void Should_Track_INCC_Replace() public void Should_Track_INCC_Replace()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = CreateTarget(data, o => o.Foo[1]); var target = CreateTarget(data, o => o.Foo[1]).ToObservable();
var result = new List<object?>(); var result = new List<object?>();
using (var sub = target.Subscribe(result.Add)) 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 // 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. // as AvaloniaList as it implements PropertyChanged as an explicit interface event.
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = CreateTarget(data, o => o.Foo[1]); var target = CreateTarget(data, o => o.Foo[1]).ToObservable();
var result = new List<object?>(); var result = new List<object?>();
var sub = target.Subscribe(result.Add); var sub = target.Subscribe(result.Add);
@ -193,7 +193,7 @@ public abstract class BindingExpressionTests_Indexer
public void Should_Track_INCC_Reset() public void Should_Track_INCC_Reset()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = CreateTarget(data, o => o.Foo[1]); var target = CreateTarget(data, o => o.Foo[1]).ToObservable();
var result = new List<object?>(); var result = new List<object?>();
var sub = target.Subscribe(x => result.Add(BindingNotification.ExtractValue(x))); 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["foo"] = "bar";
data.Foo["baz"] = "qux"; 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<object?>(); var result = new List<object?>();
using (var sub = target.Subscribe(result.Add)) using (var sub = target.Subscribe(result.Add))
@ -236,9 +236,9 @@ public abstract class BindingExpressionTests_Indexer
var data = new { Foo = new[] { "foo", "bar" } }; var data = new { Foo = new[] { "foo", "bar" } };
var target = CreateTarget(data, o => o.Foo[1]); 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]); Assert.Equal("baz", data.Foo[1]);
@ -258,9 +258,9 @@ public abstract class BindingExpressionTests_Indexer
}; };
var target = CreateTarget(data, o => o.Foo["foo"]); 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"]); Assert.Equal(4, data.Foo["foo"]);
@ -280,9 +280,9 @@ public abstract class BindingExpressionTests_Indexer
}; };
var target = CreateTarget(data, o => o.Foo["bar"]); 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"]); Assert.Equal(4, data.Foo["bar"]);
@ -299,9 +299,9 @@ public abstract class BindingExpressionTests_Indexer
var target = CreateTarget(data, o => o.Foo["foo"]); 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"]); Assert.Equal("bar2", data.Foo["foo"]);
@ -314,7 +314,7 @@ public abstract class BindingExpressionTests_Indexer
{ {
var data = new[] { 1, 2, 3 }; 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); var value = await target.Take(1);

24
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) public async Task Should_Negate_Boolean_Value(bool value)
{ {
var data = new Test { Foo = 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); var result = await target.Take(1);
Assert.Equal(!value, (bool)result); 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) public async Task Should_Negate_Boolean_Value_In_Path(bool value)
{ {
var data = new Test { Next = new Test { Foo = 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); var result = await target.Take(1);
Assert.Equal(!value, (bool)result); 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) public async Task Should_Double_Negate_Boolean_Value(bool value)
{ {
var data = new Test { Foo = 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); var result = await target.Take(1);
Assert.Equal(value, (bool)result); 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) public async Task Should_Double_Negate_Boolean_Value_In_Path(bool value)
{ {
var data = new Test { Next = new Test { Foo = 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); var result = await target.Take(1);
Assert.Equal(value, (bool)result); Assert.Equal(value, (bool)result);
@ -71,9 +71,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Test { Foo = true }; var data = new Test { Foo = true };
var target = BindingExpression.Create(data, o => !o.Foo); 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); Assert.False(data.Foo);
} }
@ -83,9 +83,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Test { Next = new Test { Foo = true } }; var data = new Test { Next = new Test { Foo = true } };
var target = BindingExpression.Create(data, o => !o.Next!.Foo); 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); Assert.False(data.Next.Foo);
} }
@ -95,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Test { Foo = true }; var data = new Test { Foo = true };
var target = BindingExpression.Create(data, o => !!o.Foo); 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); Assert.False(data.Foo);
} }
@ -107,9 +107,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
{ {
var data = new Test { Next = new Test { Foo = true } }; var data = new Test { Next = new Test { Foo = true } };
var target = BindingExpression.Create(data, o => !!o.Next!.Foo); 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); Assert.False(data.Next.Foo);
} }

12
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 target = BindingExpression.Create(data, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
source.OnNext("bar"); source.OnNext("bar");
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();
@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var target = BindingExpression.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
source.OnNext("bar"); source.OnNext("bar");
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();
@ -61,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Next.StreamBinding().Foo); var target = BindingExpression.Create(data, o => o.Next.StreamBinding().Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
data.Next.OnNext(new Class2("foo")); data.Next.OnNext(new Class2("foo"));
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();
@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var target = BindingExpression.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
source.OnNext("bar"); source.OnNext("bar");
sync.ExecutePostedCallbacks(); 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 target = BindingExpression.Create(data1, o => o.Next.StreamBinding().Foo, enableDataValidation: true);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
data1.Next.OnNext(data2); data1.Next.OnNext(data2);
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();
@ -138,7 +138,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var target = BindingExpression.Create(data, o => o.Foo.StreamBinding());
var result = new List<int>(); var result = new List<int>();
var sub = target.Subscribe(x => result.Add((int)x)); var sub = target.ToObservable().Subscribe(x => result.Add((int)x));
source.OnNext(42); source.OnNext(42);
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();

32
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() public async Task Should_Get_Simple_Property_Value()
{ {
var data = new { Foo = "foo" }; 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); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -34,7 +34,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Value_Null() public async Task Should_Get_Simple_Property_Value_Null()
{ {
var data = new { Foo = (string)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); var result = await target.Take(1);
Assert.Null(result); Assert.Null(result);
@ -46,7 +46,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_From_Base_Class() public async Task Should_Get_Simple_Property_From_Base_Class()
{ {
var data = new Class3 { Foo = "foo" }; 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); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -57,7 +57,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public async Task Should_Return_BindingNotification_Error_For_Root_Null() 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); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
@ -71,7 +71,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
[Fact] [Fact]
public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue() 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); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
public async Task Should_Get_Simple_Property_Chain() public async Task Should_Get_Simple_Property_Chain()
{ {
var data = new { Foo = new { Bar = new { Baz = "baz" } } }; 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); var result = await target.Take(1);
Assert.Equal("baz", result); 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 target = BindingExpression.Create(data, o => o.Foo.Foo.Length);
var result = new List<object>(); var result = new List<object>();
target.Subscribe(x => result.Add(x)); target.ToObservable().Subscribe(x => result.Add(x));
Assert.Equal( Assert.Equal(
new[] new[]
@ -123,7 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Foo); var target = BindingExpression.Create(data, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
data.Foo = "bar"; data.Foo = "bar";
Assert.Equal(new[] { "foo", "bar" }, result); 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 target = BindingExpression.Create(data, o => o.Bar);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
Assert.Equal(new[] { "foo" }, result); 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 target = BindingExpression.Create(data, o => (o.Next as Class2).Bar);
var result = new List<object>(); var result = new List<object>();
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 = "baz";
((Class2)data.Next).Bar = null; ((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 target = BindingExpression.Create(data, o => (o.Next as Class2).Bar);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
var old = data.Next; var old = data.Next;
data.Next = new Class2 { Bar = "baz" }; data.Next = new Class2 { Bar = "baz" };
data.Next = new Class2 { Bar = null }; 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 target = BindingExpression.Create(data, o => ((o.Next as Class2).Next as Class2).Bar);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
var old = data.Next; var old = data.Next;
data.Next = new Class2 { Bar = "baz" }; data.Next = new Class2 { Bar = "baz" };
data.Next = old; 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 target = BindingExpression.Create(data, o => (o.Next as Class2).Bar);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
var old = data.Next; var old = data.Next;
var breaking = new WithoutBar(); var breaking = new WithoutBar();
data.Next = breaking; data.Next = breaking;
@ -321,7 +321,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
}; };
var result = run(); var result = run();
result.Item1.Subscribe(x => { }); result.Item1.ToObservable().Subscribe(x => { });
// Mono trickery // Mono trickery
GC.Collect(2); GC.Collect(2);
@ -341,7 +341,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(source, x => x.Name); var target = BindingExpression.Create(source, x => x.Name);
var result = new List<object>(); var result = new List<object>();
target.Subscribe(x => result.Add(x)); target.ToObservable().Subscribe(x => result.Add(x));
Assert.Equal(new[] { "NewName" }, result); Assert.Equal(new[] { "NewName" }, result);
} }
@ -357,8 +357,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
data, data,
o => o.IntValue, o => o.IntValue,
mode: BindingMode.TwoWay, mode: BindingMode.TwoWay,
target: control,
targetProperty: DockPanel.DockProperty,
allowReflection: allowReflection); allowReflection: allowReflection);
var instance = new InstancedBinding( var instance = new InstancedBinding(
target, target,

44
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 data = new Person { Name = "Frank" };
var target = BindingExpression.Create(data, o => o.Name); 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); Assert.Equal("Kups", data.Name);
@ -35,9 +35,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
var data = new AvaloniaObject(); var data = new AvaloniaObject();
var target = BindingExpression.Create(data, o => o[DockPanel.DockProperty]); 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]); Assert.Equal(Dock.Right, data[DockPanel.DockProperty]);
@ -49,9 +49,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
var data = new { Foo = new[] { "foo" } }; var data = new { Foo = new[] { "foo" } };
var target = BindingExpression.Create(data, o => o.Foo[0]); 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]); 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 data = new Person { Pet = new Dog { Name = "Fido" } };
var target = BindingExpression.Create(data, o => o.Pet!.Name); 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); 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); var target = BindingExpression.Create(data, o => o.Pet!.Name);
// Ensure the UntypedBindingExpression's subscriptions are kept active. // Ensure the UntypedBindingExpression's subscriptions are kept active.
using (target!.OfType<string?>().Subscribe(x => { })) using (target.ToObservable()!.OfType<string?>().Subscribe(x => { }))
{ {
data.Pet = null; 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 data = new Person { Pet = new Cat() };
var target = BindingExpression.Create(data, o => (o.Pet as Dog)!.IsBarky); 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); GC.KeepAlive(data);
@ -108,8 +108,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Name); var target = BindingExpression.Create(data, o => o.Name);
var result = new List<object?>(); var result = new List<object?>();
target.Subscribe(result.Add); target.ToObservable().Subscribe(result.Add);
target.SetValue("Frank"); target.WriteValueToSource("Frank");
Assert.Equal(new[] { null, "Frank" }, result); 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 target = BindingExpression.Create(data, o => o.Name);
var result = new List<object?>(); var result = new List<object?>();
target.Subscribe(result.Add); target.ToObservable().Subscribe(result.Add);
target.SetValue("Frank"); target.WriteValueToSource("Frank");
Assert.Equal(new[] { null, "Frank" }, result); Assert.Equal(new[] { null, "Frank" }, result);
@ -137,9 +137,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
var data = new Person(); var data = new Person();
var target = BindingExpression.Create(data, o => (o.Pet as Dog)!.Name); 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); GC.KeepAlive(data);
@ -154,9 +154,9 @@ namespace Avalonia.Base.UnitTests.Data.Core
o => o.Name, o => o.Name,
converter: new CaseConverter()); 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); Assert.Equal("kups", data.Name);
@ -173,9 +173,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
data, data,
o => o.WhiskerCount, o => o.WhiskerCount,
converter: new CaseConverter(), converter: new CaseConverter(),
mode: BindingMode.TwoWay, mode: BindingMode.TwoWay);
target: control,
targetProperty: Visual.OpacityProperty);
var instance = new InstancedBinding( var instance = new InstancedBinding(
target, target,
BindingMode.TwoWay, BindingMode.TwoWay,

12
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 target = BindingExpression.Create(data, o => o.Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
tcs.SetResult("foo"); tcs.SetResult("foo");
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();
@ -41,7 +41,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var target = BindingExpression.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
Assert.Equal(new[] { "foo" }, result); 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 target = BindingExpression.Create(data, o => o.Next.StreamBinding().Foo);
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
tcs.SetResult(new Class2("foo")); tcs.SetResult(new Class2("foo"));
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();
@ -79,7 +79,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var target = BindingExpression.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
tcs.SetException(new NotSupportedException()); tcs.SetException(new NotSupportedException());
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();
@ -105,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var target = BindingExpression.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
Assert.Equal( Assert.Equal(
new[] new[]
@ -130,7 +130,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
var target = BindingExpression.Create(data, o => o.Foo.StreamBinding()); var target = BindingExpression.Create(data, o => o.Foo.StreamBinding());
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
tcs.SetResult("foo"); tcs.SetResult("foo");
sync.ExecutePostedCallbacks(); sync.ExecutePostedCallbacks();

2
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -1847,7 +1847,7 @@ namespace Avalonia.Controls.UnitTests
public InstancedBinding ItemsSelector(object item) public InstancedBinding ItemsSelector(object item)
{ {
var obs = BindingExpression.Create(item, o => ((Node)o).Children); 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) public bool Match(object? data)

8
tests/Avalonia.LeakTests/BindingExpressionTests.cs

@ -26,7 +26,7 @@ namespace Avalonia.LeakTests
var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = BindingExpression.Create(source, o => o.Foo); var target = BindingExpression.Create(source, o => o.Foo);
target.Subscribe(_ => { }); target.ToObservable().Subscribe(_ => { });
return target; return target;
}; };
@ -44,7 +44,7 @@ namespace Avalonia.LeakTests
var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = BindingExpression.Create(source, o => o.Foo, enableDataValidation: true); var target = BindingExpression.Create(source, o => o.Foo, enableDataValidation: true);
target.Subscribe(_ => { }); target.ToObservable().Subscribe(_ => { });
return target; return target;
}; };
@ -62,7 +62,7 @@ namespace Avalonia.LeakTests
var source = new { Foo = new NonIntegerIndexer() }; var source = new { Foo = new NonIntegerIndexer() };
var target = BindingExpression.Create(source, o => o.Foo); var target = BindingExpression.Create(source, o => o.Foo);
target.Subscribe(_ => { }); target.ToObservable().Subscribe(_ => { });
return target; return target;
}; };
@ -79,7 +79,7 @@ namespace Avalonia.LeakTests
{ {
var source = new { Foo = new MethodBound() }; var source = new { Foo = new MethodBound() };
var target = BindingExpression.Create(source, o => (Action)o.Foo.A); var target = BindingExpression.Create(source, o => (Action)o.Foo.A);
target.Subscribe(_ => { }); target.ToObservable().Subscribe(_ => { });
return target; return target;
}; };

4
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.GetObservable(Control.DataContextProperty).Subscribe(x => values.Add(x));
child.Bind(Control.DataContextProperty, new Binding("Foo")); 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 // 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 // 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)); Assert.True(child.IsSet(Control.DataContextProperty));
root.Child = child; root.Child = child;

2
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs

@ -26,7 +26,7 @@ namespace Avalonia.Markup.UnitTests.Data
var expressionObserver = (BindingExpression)target.Initiate( var expressionObserver = (BindingExpression)target.Initiate(
textBlock, textBlock,
TextBlock.TextProperty).Source; TextBlock.TextProperty).Expression;
Assert.Same(StringConverters.IsNullOrEmpty, expressionObserver.Converter); Assert.Same(StringConverters.IsNullOrEmpty, expressionObserver.Converter);
} }

6
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs

@ -109,7 +109,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
[Fact] [Fact]
public void Should_Not_Keep_Source_Alive() public void Should_Not_Keep_Source_Alive()
{ {
Func<Tuple<BindingExpression, WeakReference>> run = () => Func<Tuple<IObservable<object>, WeakReference>> run = () =>
{ {
var source = new Class1(); var source = new Class1();
var target = Build(source, "(Owner.Foo)", typeResolver: _typeResolver); var target = Build(source, "(Owner.Foo)", typeResolver: _typeResolver);
@ -144,13 +144,13 @@ namespace Avalonia.Markup.UnitTests.Parsers
Assert.Throws<ExpressionParseException>(() => Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver)); Assert.Throws<ExpressionParseException>(() => Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver));
} }
private static BindingExpression Build(object source, string path, Func<string, string, Type> typeResolver) private static IObservable<object> Build(object source, string path, Func<string, string, Type> typeResolver)
{ {
var r = new CharacterReader(path); var r = new CharacterReader(path);
var grammar = BindingExpressionGrammar.Parse(ref r).Nodes; var grammar = BindingExpressionGrammar.Parse(ref r).Nodes;
var nodes = new List<ExpressionNode>(); var nodes = new List<ExpressionNode>();
ExpressionNodeFactory.CreateFromAst(grammar, typeResolver, null, nodes, out _); 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 private static class Owner

4
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() public async Task Should_Get_AvaloniaProperty_By_Name()
{ {
var data = new Class1(); var data = new Class1();
var target = Build(data, "Foo"); var target = Build(data, "Foo").ToObservable();
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("foo", result); Assert.Equal("foo", result);
@ -37,7 +37,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
var target = Build(data, "Foo"); var target = Build(data, "Foo");
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.ToObservable().Subscribe(x => result.Add(x));
data.SetValue(Class1.FooProperty, "bar"); data.SetValue(Class1.FooProperty, "bar");
Assert.Equal(new[] { "foo", "bar" }, result); Assert.Equal(new[] { "foo", "bar" }, result);

57
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs

@ -22,7 +22,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Get_Array_Value() public async Task Should_Get_Array_Value()
{ {
var data = new { Foo = new[] { "foo", "bar" } }; 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); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -34,7 +34,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Get_UnsetValue_For_Invalid_Array_Index() public async Task Should_Get_UnsetValue_For_Invalid_Array_Index()
{ {
var data = new { Foo = new[] { "foo", "bar" } }; 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); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); 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() public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index()
{ {
var data = new { Foo = new Dictionary<int, string> { { 1, "foo" } } }; var data = new { Foo = new Dictionary<int, string> { { 1, "foo" } } };
var target = Build(data, "Foo[invalid]"); var target = BuildAsObservable(data, "Foo[invalid]");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); 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() public async Task Should_Get_Error_For_Object_Without_Indexer()
{ {
var data = new { Foo = 5 }; var data = new { Foo = 5 };
var target = Build(data, "Foo[noindexer]"); var target = BuildAsObservable(data, "Foo[noindexer]");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
@ -74,7 +74,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Get_MultiDimensional_Array_Value() public async Task Should_Get_MultiDimensional_Array_Value()
{ {
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; 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); var result = await target.Take(1);
Assert.Equal("qux", result); Assert.Equal("qux", result);
@ -86,7 +86,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Get_Value_For_String_Indexer() public async Task Should_Get_Value_For_String_Indexer()
{ {
var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } }; var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
var target = Build(data, "Foo[foo]"); var target = BuildAsObservable(data, "Foo[foo]");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -98,7 +98,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Get_Value_For_Non_String_Indexer() public async Task Should_Get_Value_For_Non_String_Indexer()
{ {
var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } }; var data = new { Foo = new Dictionary<double, string> { { 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); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -110,7 +110,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue()
{ {
var data = new { Foo = new[] { "foo", "bar" } }; 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); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); 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() public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue()
{ {
var data = new { Foo = new[] { "foo", "bar" } }; 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); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); 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() public async Task List_Out_Of_Bounds_Should_Return_UnsetValue()
{ {
var data = new { Foo = new List<string> { "foo", "bar" } }; var data = new { Foo = new List<string> { "foo", "bar" } };
var target = Build(data, "Foo[2]"); var target = BuildAsObservable(data, "Foo[2]");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result)); Assert.Equal(AvaloniaProperty.UnsetValue, BindingNotification.ExtractValue(result));
@ -146,7 +146,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Get_List_Value() public async Task Should_Get_List_Value()
{ {
var data = new { Foo = new List<string> { "foo", "bar" } }; var data = new { Foo = new List<string> { "foo", "bar" } };
var target = Build(data, "Foo[1]"); var target = BuildAsObservable(data, "Foo[1]");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal("bar", result); Assert.Equal("bar", result);
@ -158,7 +158,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public void Should_Track_INCC_Add() public void Should_Track_INCC_Add()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = Build(data, "Foo[2]"); var target = BuildAsObservable(data, "Foo[2]");
var result = new List<object>(); var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(BindingNotification.ExtractValue(x)))) 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() public void Should_Track_INCC_Remove()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = Build(data, "Foo[0]"); var target = BuildAsObservable(data, "Foo[0]");
var result = new List<object>(); var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x))) using (var sub = target.Subscribe(x => result.Add(x)))
@ -200,7 +200,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public void Should_Track_INCC_Replace() public void Should_Track_INCC_Replace()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = Build(data, "Foo[1]"); var target = BuildAsObservable(data, "Foo[1]");
var result = new List<object>(); var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x))) 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 // 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. // as AvaloniaList as it implements PropertyChanged as an explicit interface event.
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } };
var target = Build(data, "Foo[1]"); var target = BuildAsObservable(data, "Foo[1]");
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x)); var sub = target.Subscribe(x => result.Add(x));
@ -240,7 +240,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public void Should_Track_INCC_Reset() public void Should_Track_INCC_Reset()
{ {
var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var data = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = Build(data, "Foo[1]"); var target = BuildAsObservable(data, "Foo[1]");
var result = new List<object>(); var result = new List<object>();
var sub = target.Subscribe(x => result.Add(BindingNotification.ExtractValue(x))); 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["foo"] = "bar";
data.Foo["baz"] = "qux"; data.Foo["baz"] = "qux";
var target = Build(data, "Foo[foo]"); var target = BuildAsObservable(data, "Foo[foo]");
var result = new List<object>(); var result = new List<object>();
using (var sub = target.Subscribe(x => result.Add(x))) 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 data = new { Foo = new[] { "foo", "bar" } };
var target = Build(data, "Foo[1]"); 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]); Assert.Equal("baz", data.Foo[1]);
@ -305,9 +305,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
}; };
var target = Build(data, "Foo[foo]"); 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"]); Assert.Equal(4, data.Foo["foo"]);
@ -327,9 +327,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
}; };
var target = Build(data, "Foo[bar]"); 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"]); Assert.Equal(4, data.Foo["bar"]);
@ -346,9 +346,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
var target = Build(data, "Foo[foo]"); 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"]); Assert.Equal("bar2", data.Foo["foo"]);
@ -361,7 +361,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
{ {
var data = new[] { 1, 2, 3 }; var data = new[] { 1, 2, 3 };
var target = Build(data, "[1]"); var target = BuildAsObservable(data, "[1]");
var value = await target.Take(1); var value = await target.Take(1);
@ -377,6 +377,11 @@ namespace Avalonia.Markup.UnitTests.Parsers
return new BindingExpression(source, nodes, AvaloniaProperty.UnsetValue); return new BindingExpression(source, nodes, AvaloniaProperty.UnsetValue);
} }
private static IObservable<object> BuildAsObservable(object source, string path, Func<string, string, Type> typeResolver = null)
{
return Build(source, path, typeResolver).ToObservable();
}
private class NonIntegerIndexer : NotifyingBase private class NonIntegerIndexer : NotifyingBase
{ {
private readonly Dictionary<string, string> _storage = new Dictionary<string, string>(); private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();

4
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<object> Build(object source, string path)
{ {
var r = new CharacterReader(path); var r = new CharacterReader(path);
var grammar = BindingExpressionGrammar.Parse(ref r).Nodes; var grammar = BindingExpressionGrammar.Parse(ref r).Nodes;
var nodes = new List<ExpressionNode>(); var nodes = new List<ExpressionNode>();
ExpressionNodeFactory.CreateFromAst(grammar, null, null, nodes, out _); ExpressionNodeFactory.CreateFromAst(grammar, null, null, nodes, out _);
return new BindingExpression(source, nodes, AvaloniaProperty.UnsetValue); return new BindingExpression(source, nodes, AvaloniaProperty.UnsetValue).ToObservable();
} }
} }
} }

27
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs

@ -19,7 +19,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Negate_0() public async Task Should_Negate_0()
{ {
var data = new { Foo = 0 }; var data = new { Foo = 0 };
var target = Build(data, "!Foo"); var target = BuildAsObservable(data, "!Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.True((bool)result); Assert.True((bool)result);
@ -31,7 +31,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Negate_1() public async Task Should_Negate_1()
{ {
var data = new { Foo = 1 }; var data = new { Foo = 1 };
var target = Build(data, "!Foo"); var target = BuildAsObservable(data, "!Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.False((bool)result); Assert.False((bool)result);
@ -43,7 +43,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Negate_False_String() public async Task Should_Negate_False_String()
{ {
var data = new { Foo = "false" }; var data = new { Foo = "false" };
var target = Build(data, "!Foo"); var target = BuildAsObservable(data, "!Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.True((bool)result); Assert.True((bool)result);
@ -55,7 +55,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Negate_True_String() public async Task Should_Negate_True_String()
{ {
var data = new { Foo = "True" }; var data = new { Foo = "True" };
var target = Build(data, "!Foo"); var target = BuildAsObservable(data, "!Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.False((bool)result); 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() public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{ {
var data = new { Foo = "foo" }; var data = new { Foo = "foo" };
var target = Build(data, "!Foo"); var target = BuildAsObservable(data, "!Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
@ -83,7 +83,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{ {
var data = new { Foo = new object() }; var data = new { Foo = new object() };
var target = Build(data, "!Foo"); var target = BuildAsObservable(data, "!Foo");
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
@ -99,7 +99,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Negate_BindingNotification_Value() public async Task Should_Negate_BindingNotification_Value()
{ {
var data = new { Foo = true }; 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); var result = await target.Take(1);
Assert.Equal(new BindingNotification(false), result); Assert.Equal(new BindingNotification(false), result);
@ -111,7 +111,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Pass_Through_BindingNotification_Error() public async Task Should_Pass_Through_BindingNotification_Error()
{ {
var data = new object(); var data = new object();
var target = Build(data, "!Foo", enableDataValidation: true); var target = BuildAsObservable(data, "!Foo", enableDataValidation: true);
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
@ -127,7 +127,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
public async Task Should_Negate_BindingNotification_Error_FallbackValue() public async Task Should_Negate_BindingNotification_Error_FallbackValue()
{ {
var data = new Test { DataValidationError = "Test error" }; 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); var result = await target.Take(1);
Assert.Equal( Assert.Equal(
@ -145,9 +145,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
{ {
var data = new { Foo = "foo" }; var data = new { Foo = "foo" };
var target = Build(data, "!Foo"); var target = Build(data, "!Foo");
target.Subscribe(_ => { }); target.ToObservable().Subscribe(_ => { });
Assert.False(target.SetValue("bar")); Assert.False(target.WriteValueToSource("bar"));
GC.KeepAlive(data); GC.KeepAlive(data);
} }
@ -165,6 +165,11 @@ namespace Avalonia.Markup.UnitTests.Parsers
enableDataValidation: enableDataValidation); enableDataValidation: enableDataValidation);
} }
private static IObservable<object> BuildAsObservable(object source, string path, bool enableDataValidation = false)
{
return Build(source, path, enableDataValidation).ToObservable();
}
private class Test : INotifyDataErrorInfo private class Test : INotifyDataErrorInfo
{ {
private string _dataValidationError; private string _dataValidationError;

2
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() public async Task Should_Return_BindingNotification_Error_For_Broken_Chain()
{ {
var data = new { Foo = new { Bar = 1 } }; 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); var result = await target.Take(1);
Assert.IsType<BindingNotification>(result); Assert.IsType<BindingNotification>(result);

Loading…
Cancel
Save