Browse Source

Use BindingExpression directly in ValueStoe.

refactor/bindingexpressions-in-valuestore
Steven Kirk 3 years ago
parent
commit
15ab324e02
  1. 9
      src/Avalonia.Base/AvaloniaObject.cs
  2. 19
      src/Avalonia.Base/AvaloniaProperty.cs
  3. 2
      src/Avalonia.Base/Data/BindingOperations.cs
  4. 39
      src/Avalonia.Base/Data/BindingValue.cs
  5. 56
      src/Avalonia.Base/Data/Core/BindingExpression.Observable.cs
  6. 33
      src/Avalonia.Base/Data/Core/BindingExpression.ValueEntry.cs
  7. 396
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  8. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs
  9. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs
  10. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs
  11. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs
  12. 74
      src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs
  13. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs
  14. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs
  15. 4
      src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs
  16. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs
  17. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/NamedElementNode.cs
  18. 6
      src/Avalonia.Base/Data/Core/ExpressionNodes/ParentDataContextNode.cs
  19. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/PropertyAccessorNode.cs
  20. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs
  21. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs
  22. 4
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs
  23. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs
  24. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs
  25. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs
  26. 2
      src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs
  27. 20
      src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs
  28. 7
      src/Avalonia.Base/DirectProperty.cs
  29. 12
      src/Avalonia.Base/DirectPropertyBase.cs
  30. 6
      src/Avalonia.Base/IDirectPropertyAccessor.cs
  31. 12
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  32. 18
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  33. 9
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  34. 137
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  35. 18
      src/Avalonia.Base/StyledProperty.cs
  36. 9
      src/Avalonia.Base/Styling/Setter.cs
  37. 6
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  38. 6
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  39. 6
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs
  40. 4
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs

9
src/Avalonia.Base/AvaloniaObject.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.PropertyStore;
@ -608,6 +609,14 @@ namespace Avalonia
internal ValueStore GetValueStore() => _values;
internal IReadOnlyList<AvaloniaObject>? GetInheritanceChildren() => _inheritanceChildren;
internal IDisposable Bind(
AvaloniaProperty property,
BindingExpression expression,
BindingPriority priority)
{
return property.RouteBind(this, expression, priority);
}
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.

19
src/Avalonia.Base/AvaloniaProperty.cs

@ -542,6 +542,14 @@ namespace Avalonia
/// <param name="value">The value.</param>
internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value);
/// <summary>
/// Routes an untyped SetDirectValueUnchecked call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
/// <param name="value">The value.</param>
internal virtual void RouteSetDirectValueUnchecked(AvaloniaObject o, object? value) =>
throw new NotSupportedException();
/// <summary>
/// Routes an untyped Bind call to a typed call.
/// </summary>
@ -553,6 +561,17 @@ namespace Avalonia
IObservable<object?> source,
BindingPriority priority);
/// <summary>
/// Routes an untyped Bind call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
/// <param name="source">The binding source.</param>
/// <param name="priority">The priority.</param>
internal abstract IDisposable RouteBind(
AvaloniaObject o,
BindingExpression source,
BindingPriority priority);
/// <summary>
/// Overrides the metadata for the property on the specified type.
/// </summary>

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

@ -26,7 +26,7 @@ namespace Avalonia.Data
if (binding.Source is BindingExpression expression)
{
return target.Bind(property, expression);
return target.Bind(property, expression, BindingPriority.LocalValue);
}
var mode = binding.Mode;

39
src/Avalonia.Base/Data/BindingValue.cs

@ -415,6 +415,45 @@ namespace Avalonia.Data
e);
}
/// <summary>
/// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
/// <see cref="AvaloniaProperty.UnsetValue"/>, <see cref="BindingOperations.DoNothing"/> and
/// <see cref="BindingNotification"/> without type conversion.
/// </summary>
/// <param name="value">The untyped value.</param>
/// <returns>The typed binding value.</returns>
internal static BindingValue<T> FromUntypedStrict(object? value)
{
if (value == AvaloniaProperty.UnsetValue)
return Unset;
else if (value == BindingOperations.DoNothing)
return DoNothing;
var type = BindingValueType.Value;
T? v = default;
Exception? error = null;
if (value is BindingNotification n)
{
error = n.Error;
type = n.ErrorType switch
{
BindingErrorType.Error => BindingValueType.BindingError,
BindingErrorType.DataValidationError => BindingValueType.DataValidationError,
_ => BindingValueType.Value,
};
if (n.HasValue)
type |= BindingValueType.HasValue;
value = n.Value;
}
if ((type & BindingValueType.HasValue) != 0)
v = (T)value!;
return new BindingValue<T>(type, v, error);
}
[Conditional("DEBUG")]
private static void ValidateValue(T value)
{

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

@ -0,0 +1,56 @@
using System;
namespace Avalonia.Data.Core;
// Mostly for unit tests, BindingExpression implements IObservable<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

@ -0,0 +1,33 @@
using System;
using Avalonia.PropertyStore;
using Avalonia.Styling;
namespace Avalonia.Data.Core;
internal partial class BindingExpression : IValueEntry, ISetterInstance
{
bool IValueEntry.HasValue
{
get
{
Start(produceValue: false);
return _value is not null;
}
}
AvaloniaProperty IValueEntry.Property => _targetProperty ?? throw new Exception();
bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
{
GetDataValidationState(out state, out error);
return IsDataValidationEnabled;
}
object? IValueEntry.GetValue()
{
Start(produceValue: false);
return GetValueOrDefault();
}
void IValueEntry.Unsubscribe() => Stop();
}

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

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq.Expressions;
@ -8,7 +9,7 @@ using Avalonia.Data.Converters;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
using Avalonia.Logging;
using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
using Avalonia.Threading;
namespace Avalonia.Data.Core;
@ -19,21 +20,21 @@ namespace Avalonia.Data.Core;
/// A <see cref="BindingExpression"/> represents a untyped binding which has been
/// instantiated on an object.
/// </remarks>
internal class BindingExpression : IObservable<object?>,
IObserver<object?>,
IDescription,
IDisposable
internal partial class BindingExpression : IDescription, IDisposable
{
internal static readonly WeakReference<object?> NullReference = new(null);
private readonly WeakReference<object?>? _source;
private readonly WeakReference<AvaloniaObject?> _target;
private readonly BindingMode _mode;
private readonly IReadOnlyList<ExpressionNode> _nodes;
private readonly AvaloniaProperty? _targetProperty;
private readonly TargetTypeConverter? _targetTypeConverter;
private readonly bool _enableDataValidation;
private IObserver<object?>? _observer;
private bool _isRunning;
private BindingPriority _priority;
private bool _produceValue;
private IBindingExpressionSink? _sink;
private AvaloniaProperty? _targetProperty;
private WeakReference<object?>? _value;
private BindingError? _error;
private UncommonFields? _uncommon;
/// <summary>
@ -89,7 +90,7 @@ internal class BindingExpression : IObservable<object?>,
_mode = mode;
_nodes = nodes;
_targetTypeConverter = targetTypeConverter;
_enableDataValidation = enableDataValidation;
IsDataValidationEnabled = enableDataValidation;
if (converter is not null ||
converterCulture is not null ||
@ -138,16 +139,83 @@ internal class BindingExpression : IObservable<object?>,
}
}
public BindingPriority Priority => _priority;
public Type? SourceType => (LeafNode as ISettableNode)?.ValueType;
public AvaloniaProperty? TargetProperty => _targetProperty;
public Type TargetType => _targetProperty?.PropertyType ?? typeof(object);
public IValueConverter? Converter => _uncommon?._converter;
public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture;
public object? ConverterParameter => _uncommon?._converterParameter;
public object? FallbackValue => _uncommon is not null ? _uncommon._fallbackValue : AvaloniaProperty.UnsetValue;
public bool IsDataValidationEnabled { get; }
public bool HasDataValidationError => _error?.ErrorType == BindingValueType.DataValidationError;
public object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue;
public ExpressionNode LeafNode => _nodes[_nodes.Count - 1];
public string? StringFormat => _uncommon?._stringFormat;
/// <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>
/// Writes the specified value to the binding source if possible.
/// </summary>
@ -177,13 +245,13 @@ internal class BindingExpression : IObservable<object?>,
{
value = FallbackValue;
}
else if (_enableDataValidation)
else if (IsDataValidationEnabled)
{
var valueString = value?.ToString() ?? "(null)";
var valueTypeName = value?.GetType().FullName ?? "null";
var ex = new InvalidCastException(
$"Could not convert '{valueString}' ({valueTypeName}) to {type}.");
_observer?.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError));
OnDataValidationError(ex);
return false;
}
else
@ -206,6 +274,61 @@ internal class BindingExpression : IObservable<object?>,
}
}
/// <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>
/// Creates an <see cref="BindingExpression"/> from an expression tree.
/// </summary>
@ -258,57 +381,26 @@ internal class BindingExpression : IObservable<object?>,
TargetTypeConverter.GetReflectionConverter() :
TargetTypeConverter.GetDefaultConverter());
}
/// <summary>
/// Implements the disposable returned by <see cref="IObservable{T}.Subscribe(IObserver{T})"/>.
/// </summary>
void IDisposable.Dispose()
{
if (_observer is null)
return;
_observer = null;
Stop();
}
IDisposable IObservable<object?>.Subscribe(IObserver<object?> observer)
{
if (_observer is not null)
throw new InvalidOperationException(
$"An {nameof(BindingExpression)} may only have a single subscriber.");
_observer = observer ?? throw new ArgumentNullException(nameof(observer));
Start();
return this;
}
void IObserver<object?>.OnCompleted() { }
void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.OnNext(object? value) => SetValue(value);
/// <summary>
/// Called by an <see cref="ExpressionNode"/> belonging to this binding when its
/// <see cref="ExpressionNode.Value"/> changes.
/// </summary>
/// <param name="nodeIndex">The <see cref="ExpressionNode.Index"/>.</param>
/// <param name="value">The <see cref="ExpressionNode.Value"/>.</param>
internal void OnNodeValueChanged(int nodeIndex, object? value)
/// <param name="dataValidationError">
/// The data validation error associated with the current value, if any.
/// </param>
internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataValidationError)
{
if (value is BindingNotification notification &&
notification.ErrorType == BindingErrorType.Error &&
notification.Error is not null &&
ShouldLogError(out var target))
{
// Log any errors the arrive via a node value change. This is mainly to make sure that
// errors which come from property accessors get logged.
Log(target, notification.Error.Message, CalculateErrorPoint(nodeIndex));
}
Debug.Assert(value is not BindingNotification);
if (nodeIndex == _nodes.Count - 1)
{
// The leaf node has changed. If the binding mode is not OneWayToSource, publish the
// value to the target.
if (_mode != BindingMode.OneWayToSource)
PublishValue();
UpdateAndPublishValue(value, dataValidationError);
// If the binding mode is OneTime, then stop the binding.
if (_mode == BindingMode.OneTime)
@ -319,7 +411,7 @@ internal class BindingExpression : IObservable<object?>,
// When the binding mode is OneWayToSource, we need to write the value to the source
// when the object holding the source property changes; this is node before the leaf
// node. First update the leaf node's source, then write the value to its property.
_nodes[nodeIndex + 1].SetSource(value);
_nodes[nodeIndex + 1].SetSource(value, dataValidationError);
WriteTargetValueToSource();
}
else if (value is null)
@ -328,7 +420,7 @@ internal class BindingExpression : IObservable<object?>,
}
else
{
_nodes[nodeIndex + 1].SetSource(value);
_nodes[nodeIndex + 1].SetSource(value, dataValidationError);
}
}
@ -342,15 +434,13 @@ internal class BindingExpression : IObservable<object?>,
/// <param name="error">The error message.</param>
internal void OnNodeError(int nodeIndex, string error)
{
_value = null;
// Set the source of all nodes after the one that errored to null. This needs to be done
// for each node individually because setting the source to null will not result in
// OnNodeValueChanged or OnNodeError being called.
for (var i = nodeIndex + 1; i < _nodes.Count; ++i)
_nodes[i].SetSource(null);
_nodes[i].SetSource(null, null);
if (_observer is null || _mode == BindingMode.OneWayToSource)
if (_mode == BindingMode.OneWayToSource)
return;
var errorPoint = CalculateErrorPoint(nodeIndex);
@ -358,11 +448,21 @@ internal class BindingExpression : IObservable<object?>,
if (ShouldLogError(out var target))
Log(target, error, errorPoint);
var e = new BindingChainException(error, Description, errorPoint.ToString());
_observer.OnNext(new BindingNotification(
e,
BindingErrorType.Error,
ConvertFallback(FallbackValue, nameof(FallbackValue))));
// Clear the current value.
UpdateValue(AvaloniaProperty.UnsetValue, null, out var hasValueChanged, out _);
// And store the error.
_error = new(
new BindingChainException(error, Description, errorPoint.ToString()),
BindingValueType.BindingError);
PublishValue(hasValueChanged, hasErrorChanged: true);
}
internal void OnDataValidationError(Exception error)
{
_error = new(error, BindingValueType.DataValidationError);
PublishValue(hasValueChanged: false, hasErrorChanged: true);
}
private string CalculateErrorPoint(int nodeIndex)
@ -378,6 +478,27 @@ internal class BindingExpression : IObservable<object?>,
return result.ToString();
}
private object? GetCachedDefaultValue()
{
Debug.Assert(_targetProperty is not null);
if (_uncommon?._isDefaultValueInitialized == true)
return _uncommon._defaultValue;
if (_target.TryGetTarget(out var target))
{
_uncommon ??= new();
_uncommon._isDefaultValueInitialized = true;
if (_targetProperty.IsDirect)
_uncommon._defaultValue = ((IDirectPropertyAccessor)_targetProperty).GetUnsetValue(target.GetType());
else
_uncommon._defaultValue = ((IStyledPropertyAccessor)_targetProperty).GetDefaultValue(target.GetType());
}
return _uncommon?._defaultValue ?? AvaloniaProperty.UnsetValue;
}
private void Log(AvaloniaObject target, string error, LogEventLevel level = LogEventLevel.Warning)
{
if (!Logger.TryGet(level, LogArea.Binding, out var log))
@ -414,17 +535,28 @@ internal class BindingExpression : IObservable<object?>,
return true;
}
private void Start()
private void Start(bool produceValue)
{
if (_observer is null)
Debug.Assert(_sink is not null);
if (_isRunning)
return;
_isRunning = true;
_produceValue = produceValue;
if (_source?.TryGetTarget(out var source) == true)
{
if (_nodes.Count > 0)
_nodes[0].SetSource(source);
{
_nodes[0].SetSource(source, null);
}
else
_observer.OnNext(source);
{
_value = new(source);
_error = null;
PublishValue(hasValueChanged: true, hasErrorChanged: false);
}
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource &&
_target.TryGetTarget(out var target) &&
@ -440,6 +572,8 @@ internal class BindingExpression : IObservable<object?>,
{
OnNodeError(-1, "Binding Source is null.");
}
_produceValue = true;
}
private void Stop()
@ -452,42 +586,50 @@ internal class BindingExpression : IObservable<object?>,
{
target.PropertyChanged -= OnTargetPropertyChanged;
}
_isRunning = false;
_value = null;
}
private void PublishValue()
private void UpdateValue(
object? value,
Exception? dataValidationError,
out bool hasValueChanged,
out bool hasErrorChanged)
{
if (_observer is null)
return;
// The value can be a simple value or a BindingNotification. As we move through this method
// we'll keep `notification` updated with the value and current error state by calling
// `UpdateAndUnwrap`.
var valueOrNotification = _nodes.Count > 0 ? _nodes[_nodes.Count - 1].Value : null;
var value = BindingNotification.ExtractValue(valueOrNotification);
var notification = valueOrNotification as BindingNotification;
var isTargetNullValue = false;
var hadError = _error is not null;
// All values other than DoNothing should be passed to the converter.
if (value != BindingOperations.DoNothing && Converter is { } converter)
{
value = UpdateAndUnwrap(
Convert(
converter,
ConverterParameter,
value,
_targetProperty?.PropertyType ?? typeof(object)),
ref notification);
}
value = Convert(converter, ConverterParameter, value, TargetType);
// Check this here as the converter may return DoNothing.
if (value == BindingOperations.DoNothing)
{
hasValueChanged = hasErrorChanged = false;
return;
}
// Set the data validation error.
_error = dataValidationError is not null ?
new(dataValidationError, BindingValueType.DataValidationError) :
null;
// If we have a data validation error and the value is Unset then we keep the
// current value.
if (dataValidationError is not null && value == AvaloniaProperty.UnsetValue)
{
hasValueChanged = false;
hasErrorChanged = true;
return;
}
// TargetNullValue only applies when the value is null: UnsetValue indicates that there
// was a binding error so we don't want to use TargetNullValue in that case.
if (value is null && TargetNullValue != AvaloniaProperty.UnsetValue)
{
value = UpdateAndUnwrap(ConvertFallback(TargetNullValue, nameof(TargetNullValue)), ref notification);
value = ConvertFallback(TargetNullValue, nameof(TargetNullValue));
isTargetNullValue = true;
}
@ -502,21 +644,63 @@ internal class BindingExpression : IObservable<object?>,
// and the value isn't the TargetNullValue.
value = string.Format(ConverterCulture, stringFormat, value);
}
else if (_targetTypeConverter is not null && value is not null)
else if (_targetTypeConverter is not null)
{
// Otherwise, if we have a target type converter, convert the value to the target type.
value = UpdateAndUnwrap(ConvertFrom(_targetTypeConverter, value), ref notification);
value = ConvertFrom(_targetTypeConverter, value);
}
}
// FallbackValue applies if the result from the binding, converter or target type converter
// is UnsetValue.
if (value == AvaloniaProperty.UnsetValue && FallbackValue != AvaloniaProperty.UnsetValue)
value = UpdateAndUnwrap(ConvertFallback(FallbackValue, nameof(FallbackValue)), ref notification);
value = ConvertFallback(FallbackValue, nameof(FallbackValue));
// Update the stored value.
var oldValue = _value;
if (value is null)
_value = NullReference;
else
_value = new(value);
hasValueChanged = !Equals(oldValue, _value);
hasErrorChanged = _error is not null || (_error is null && hadError);
}
private void PublishValue(bool hasValueChanged, bool hasErrorChanged)
{
if (!hasValueChanged && !hasErrorChanged)
return;
if (_sink is not null && _produceValue)
{
if (Dispatcher.UIThread.CheckAccess())
{
_sink.OnChanged(this, hasValueChanged, hasErrorChanged);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var sink = _sink;
var v = hasValueChanged;
var e = hasErrorChanged;
Dispatcher.UIThread.Post(() => sink.OnChanged(this, v, e));
}
}
}
// Store the value and publish the notification/value to the observer.
_value = value is null ? NullReference : new(value);
_observer.OnNext(notification ?? value);
private void UpdateAndPublishValue(object? value, Exception? dataValidationError)
{
UpdateValue(
value,
dataValidationError,
out var hasValueChanged,
out var hasErrorChanged);
if (hasValueChanged || hasErrorChanged)
PublishValue(hasValueChanged, hasErrorChanged);
}
private void WriteTargetValueToSource()
@ -536,7 +720,7 @@ internal class BindingExpression : IObservable<object?>,
private void OnSourceChanged(object? source)
{
if (_nodes.Count > 0)
_nodes[0].SetSource(source);
_nodes[0].SetSource(source, null);
}
private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
@ -566,8 +750,8 @@ internal class BindingExpression : IObservable<object?>,
if (ShouldLogError(out var target))
Log(target, $"{message}: {e.Message}", LogEventLevel.Warning);
var ex = new InvalidCastException(message + '.', e);
return new BindingNotification(ex, BindingErrorType.Error);
_error = new(new InvalidCastException(message + '.', e), BindingValueType.BindingError);
return AvaloniaProperty.UnsetValue;
}
}
@ -585,7 +769,7 @@ internal class BindingExpression : IObservable<object?>,
return AvaloniaProperty.UnsetValue;
}
private object? ConvertFrom(TargetTypeConverter? converter, object value)
private object? ConvertFrom(TargetTypeConverter? converter, object? value)
{
if (converter is null || _targetProperty is null)
return value;
@ -602,7 +786,8 @@ internal class BindingExpression : IObservable<object?>,
if (ShouldLogError(out var target))
Log(target, message, LogEventLevel.Warning);
return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error);
_error = new(new InvalidCastException(message), BindingValueType.BindingError);
return AvaloniaProperty.UnsetValue;
}
private static bool IdentityEquals(object? a, object? b, Type type)
@ -613,23 +798,16 @@ internal class BindingExpression : IObservable<object?>,
return ReferenceEquals(a, b);
}
private static object? UpdateAndUnwrap(object? value, ref BindingNotification? notification)
private class BindingError
{
if (value is BindingNotification n)
public BindingError(Exception exception, BindingValueType errorType)
{
value = n.Value;
if (n.Error is not null)
{
if (notification is null)
notification = n;
else
notification.AddError(n.Error, n.ErrorType);
}
Exception = exception;
ErrorType = errorType;
}
notification?.SetValue(value);
return value;
public Exception Exception { get; }
public BindingValueType ErrorType { get; }
}
private class UncommonFields
@ -640,5 +818,7 @@ internal class BindingExpression : IObservable<object?>,
public object? _fallbackValue;
public string? _stringFormat;
public object? _targetNullValue;
public object? _defaultValue;
public bool _isDefaultValueInitialized;
}
}

2
src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs

@ -30,7 +30,7 @@ internal class ArrayIndexerNode : ExpressionNode
builder.Append(']');
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is Array array)
SetValue(array.GetValue(_indexes));

2
src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs

@ -35,7 +35,7 @@ internal class AvaloniaPropertyAccessorNode : ExpressionNode, ISettableNode
return false;
}
protected override void OnSourceChanged(object? source)
protected override void OnSourceChanged(object? source, Exception? dataValidationError)
{
if (source is AvaloniaObject newObject)
{

2
src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs

@ -22,7 +22,7 @@ internal abstract class CollectionNodeBase : ExpressionNode,
UpdateValueOrSetError(sender);
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
Subscribe(source);
UpdateValue(source);

2
src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs

@ -4,7 +4,7 @@ namespace Avalonia.Data.Core.ExpressionNodes;
internal class DataContextNode : DataContextNodeBase
{
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is IDataContextProvider && source is AvaloniaObject ao)
{

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

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
@ -86,7 +87,7 @@ internal abstract class ExpressionNode
/// </summary>
public void Reset()
{
SetSource(null);
SetSource(null, null);
_source = _value = null;
}
@ -116,7 +117,10 @@ internal abstract class ExpressionNode
/// <see cref="AvaloniaProperty.UnsetValue"/> in which case the source will be considered
/// to be null.
/// </param>
public void SetSource(object? source)
/// <param name="dataValidationError">
/// Any data validation error reported by the previous expression node.
/// </param>
public void SetSource(object? source, Exception? dataValidationError)
{
var oldSource = Source;
@ -137,7 +141,7 @@ internal abstract class ExpressionNode
}
else if (source != oldSource)
{
try { OnSourceChanged(source); }
try { OnSourceChanged(source, dataValidationError); }
catch (Exception e) { SetError(e); }
}
}
@ -147,6 +151,17 @@ internal abstract class ExpressionNode
/// </summary>
protected void ClearValue() => SetValue(AvaloniaProperty.UnsetValue);
/// <summary>
/// Notifies the <see cref="Owner"/> of a data validation error.
/// </summary>
/// <param name="error">The error.</param>
protected void SetDataValidationError(Exception error)
{
if (error is TargetInvocationException tie)
error = tie.InnerException!;
Owner?.OnDataValidationError(error);
}
/// <summary>
/// Sets the current value to <see cref="AvaloniaProperty.UnsetValue"/> and notifies the
/// <see cref="Owner"/> of the error.
@ -167,6 +182,8 @@ internal abstract class ExpressionNode
{
if (e is TargetInvocationException tie)
e = tie.InnerException!;
if (e is AggregateException ae && ae.InnerExceptions.Count == 1)
e = e.InnerException!;
SetError(e.Message);
}
@ -174,22 +191,58 @@ internal abstract class ExpressionNode
/// Sets the current <see cref="Value"/>, notifying the <see cref="Owner"/> if the value
/// has changed.
/// </summary>
/// <param name="value">The new value.</param>
protected void SetValue(object? value)
/// <param name="valueOrNotification">
/// The new value. May be a <see cref="BindingNotification"/>.
/// </param>
protected void SetValue(object? valueOrNotification)
{
if (valueOrNotification is BindingNotification notification)
{
if (notification.ErrorType == BindingErrorType.Error)
SetError(notification.Error!);
else if (notification.ErrorType == BindingErrorType.DataValidationError)
SetValue(notification.Value, notification.Error);
else
SetValue(notification.Value, null);
}
else
{
SetValue(valueOrNotification, null);
}
}
/// <summary>
/// Sets the current <see cref="Value"/>, notifying the <see cref="Owner"/> if the value
/// has changed.
/// </summary>
/// <param name="value">
/// The new value. May not be a <see cref="BindingNotification"/>.
/// </param>
/// <param name="dataValidationError">
/// The data validation error associated with the new value, if any.
/// </param>
protected void SetValue(object? value, Exception? dataValidationError = null)
{
Debug.Assert(value is not BindingNotification);
if (Owner is null)
return;
// We raise a change notification if:
//
// - This is the initial value (_value is null)
// - The value is a binding notification
// - There is a data validation error
// - There is no data validation error, but the owner has one
// - The old value has been GC'd - in this case we don't know if the new value is different
// - The new value is different to the old value
if (_value is null ||
value is BindingNotification ||
dataValidationError is not null ||
(dataValidationError is null && Owner.HasDataValidationError) ||
_value.TryGetTarget(out var oldValue) == false ||
!Equals(oldValue, value))
{
_value = value is null ? BindingExpression.NullReference : new(value);
Owner?.OnNodeValueChanged(Index, value);
Owner.OnNodeValueChanged(Index, value, dataValidationError);
}
}
@ -198,7 +251,10 @@ internal abstract class ExpressionNode
/// <see cref="Value"/>.
/// </summary>
/// <param name="source">The new source.</param>
protected abstract void OnSourceChanged(object source);
/// <param name="dataValidationError">
/// Any data validation error reported by the previous expression node.
/// </param>
protected abstract void OnSourceChanged(object source, Exception? dataValidationError);
/// <summary>
/// When implemented in a derived class, unsubscribes from the previous source.

2
src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs

@ -21,7 +21,7 @@ internal class FuncTransformNode : ExpressionNode
// We don't have enough information to add anything here.
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
SetValue(_transform(source));
}

2
src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs

@ -56,7 +56,7 @@ internal class LogicalAncestorElementNode : SourceNode
return target is ILogical logical && logical.IsAttachedToLogicalTree;
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is ILogical logical)
{

4
src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs

@ -28,13 +28,13 @@ internal class LogicalNotNode : ExpressionNode, ISettableNode
return false;
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
var v = BindingNotification.ExtractValue(source);
if (TryConvert(v, out var value))
{
SetValue(BindingNotification.UpdateValue(source, !value));
SetValue(BindingNotification.UpdateValue(source, !value), dataValidationError);
}
else
SetError($"Unable to convert '{source}' to bool.");

2
src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs

@ -38,7 +38,7 @@ internal class MethodCommandNode : ExpressionNode
builder.Append("()");
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is INotifyPropertyChanged newInpc)
newInpc.PropertyChanged += OnPropertyChanged;

2
src/Avalonia.Base/Data/Core/ExpressionNodes/NamedElementNode.cs

@ -30,7 +30,7 @@ internal class NamedElementNode : SourceNode
return target is not ILogical logical || logical.IsAttachedToLogicalTree;
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (_nameScope.TryGetTarget(out var scope))
_subscription = NameScopeLocator.Track(scope, _name).Subscribe(SetValue);

6
src/Avalonia.Base/Data/Core/ExpressionNodes/ParentDataContextNode.cs

@ -1,4 +1,6 @@
namespace Avalonia.Data.Core.ExpressionNodes;
using System;
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// A node in an <see cref="BindingExpression"/> which selects the value of the visual
@ -9,7 +11,7 @@ internal class ParentDataContextNode : DataContextNodeBase
private static readonly AvaloniaObject s_unset = new();
private AvaloniaObject? _parent = s_unset;
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is AvaloniaObject newElement)
newElement.PropertyChanged += OnPropertyChanged;

2
src/Avalonia.Base/Data/Core/ExpressionNodes/PropertyAccessorNode.cs

@ -45,7 +45,7 @@ internal class PropertyAccessorNode : ExpressionNode, IPropertyAccessorNode, ISe
return false;
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (_plugin.Start(new(source), PropertyName) is { } accessor)
{

2
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs

@ -42,7 +42,7 @@ internal class DynamicPluginPropertyAccessorNode : ExpressionNode, IPropertyAcce
return _accessor?.SetValue(value, BindingPriority.LocalValue) ?? false;
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
var reference = new WeakReference<object?>(source);

2
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs

@ -16,7 +16,7 @@ internal class DynamicPluginStreamNode : ExpressionNode
builder.Append('^');
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
var reference = new WeakReference<object?>(source);

4
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs

@ -52,7 +52,7 @@ internal class ReflectionIndexerNode : CollectionNodeBase, ISettableNode
return true;
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
_indexes = null;
@ -67,7 +67,7 @@ internal class ReflectionIndexerNode : CollectionNodeBase, ISettableNode
}
_indexes = ConvertIndexes(parameters, Arguments);
base.OnSourceChanged(source);
base.OnSourceChanged(source, dataValidationError);
}
else
{

2
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs

@ -19,7 +19,7 @@ internal class ReflectionTypeCastNode : ExpressionNode
builder.Append(')');
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (_targetType.IsInstanceOfType(source))
SetValue(source);

2
src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs

@ -23,7 +23,7 @@ internal class StreamNode : ExpressionNode, IObserver<object?>
void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.OnNext(object? value) => SetValue(value);
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (_plugin.Start(new(source)) is { } accessor)
{

2
src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs

@ -22,7 +22,7 @@ internal class TemplatedParentNode : SourceNode
throw new InvalidOperationException("Cannot find a StyledElement to get a TemplatedParent.");
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is StyledElement newElement)
{

2
src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs

@ -56,7 +56,7 @@ internal class VisualAncestorElementNode : SourceNode
return target is Visual visual && visual.IsAttachedToVisualTree;
}
protected override void OnSourceChanged(object source)
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is Visual visual)
{

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

@ -0,0 +1,20 @@
namespace Avalonia.Data.Core;
internal interface IBindingExpressionSink
{
/// <summary>
/// Called when a <see cref="BindingExpression"/>'s value or error state
/// changes.
/// </summary>
/// <param name="instance">The binding expression.</param>
void OnChanged(
BindingExpression instance,
bool hasValueChanged,
bool hasErrorChanged);
/// <summary>
/// Called when a <see cref="BindingExpression"/> completes.
/// </summary>
/// <param name="instance">The binding expression.</param>
void OnCompleted(BindingExpression instance);
}

7
src/Avalonia.Base/DirectProperty.cs

@ -1,5 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
namespace Avalonia
@ -142,5 +141,11 @@ namespace Avalonia
Setter((TOwner)instance, (TValue)value!);
}
object? IDirectPropertyAccessor.GetUnsetValue(Type type)
{
var metadata = GetMetadata(type);
return metadata.UnsetValue;
}
}
}

12
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore;
namespace Avalonia
@ -157,6 +158,12 @@ namespace Avalonia
return null;
}
internal override void RouteSetDirectValueUnchecked(AvaloniaObject o, object? value)
{
var bindingValue = BindingValue<TValue>.FromUntypedStrict(value);
o.SetDirectValueUnchecked<TValue>(this, bindingValue);
}
internal override void RouteSetCurrentValue(AvaloniaObject o, object? value)
{
RouteSetValue(o, value, BindingPriority.LocalValue);
@ -175,5 +182,10 @@ namespace Avalonia
{
return o.Bind(this, source);
}
internal override IDisposable RouteBind(AvaloniaObject o, BindingExpression source, BindingPriority priority)
{
return o.GetValueStore().AddBinding(this, source);
}
}
}

6
src/Avalonia.Base/IDirectPropertyAccessor.cs

@ -32,5 +32,11 @@ namespace Avalonia
/// <param name="instance">The instance.</param>
/// <param name="value">The value.</param>
void SetValue(AvaloniaObject instance, object? value);
/// <summary>
/// Gets the unset value of the property for the specified type.
/// </summary>
/// <param name="type">The type.</param>
object? GetUnsetValue(Type type);
}
}

12
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@ -127,6 +127,18 @@ namespace Avalonia.PropertyStore
IValueEntry value,
BindingPriority priority);
/// <summary>
/// Sets the value and base value for a LocalValue priority, raising
/// <see cref="AvaloniaObject.PropertyChanged"/> where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property being changed.</param>
/// <param name="value">The new value of the property.</param>
public abstract void SetLocalValueAndRaise(
ValueStore owner,
AvaloniaProperty property,
object? value);
/// <summary>
/// Raises <see cref="AvaloniaObject.PropertyChanged"/> in response to an inherited value
/// change.

18
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -66,6 +66,14 @@ namespace Avalonia.PropertyStore
}
}
public override void SetLocalValueAndRaise(
ValueStore owner,
AvaloniaProperty property,
object? value)
{
SetLocalValueAndRaise(owner, (StyledProperty<T>)property, (T)value!);
}
public void SetLocalValueAndRaise(
ValueStore owner,
StyledProperty<T> property,
@ -138,6 +146,10 @@ namespace Avalonia.PropertyStore
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
{
var clearDataValidation = ValueEntry?.GetDataValidationState(out _, out _) ??
BaseValueEntry?.GetDataValidationState(out _, out _) ??
false;
ValueEntry?.Unsubscribe();
BaseValueEntry?.Unsubscribe();
@ -163,12 +175,8 @@ namespace Avalonia.PropertyStore
owner.OnInheritedEffectiveValueDisposed(p, Value, newValue);
}
if (ValueEntry?.GetDataValidationState(out _, out _) ??
BaseValueEntry?.GetDataValidationState(out _, out _) ??
false)
{
if (clearDataValidation)
owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null);
}
}
protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property)

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

@ -1,5 +1,6 @@
using System;
using Avalonia.Data;
using Avalonia.Data.Core;
namespace Avalonia.PropertyStore
{
@ -14,6 +15,14 @@ namespace Avalonia.PropertyStore
{
}
public IValueEntry AddBinding<T>(
StyledProperty<T> property,
BindingExpression source)
{
Add(source);
return source;
}
public TypedBindingEntry<T> AddBinding<T>(
StyledProperty<T> property,
IObservable<BindingValue<T>> source)

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

@ -4,13 +4,15 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Diagnostics;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore
{
internal class ValueStore
internal class ValueStore : IBindingExpressionSink
{
private readonly List<ValueFrame> _frames = new();
private Dictionary<int, IDisposable>? _localValueBindings;
@ -41,6 +43,33 @@ namespace Avalonia.PropertyStore
ReevaluateEffectiveValues();
}
public IDisposable AddBinding<T>(
StyledProperty<T> property,
BindingExpression source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = source;
source.Start(this, property, priority);
return source;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
source.Start(this, property, priority);
return source;
}
}
public IDisposable AddBinding<T>(
StyledProperty<T> property,
IObservable<BindingValue<T>> source,
@ -125,6 +154,15 @@ namespace Avalonia.PropertyStore
}
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, BindingExpression source)
{
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = source;
source.Start(this, property, BindingPriority.LocalValue);
return source;
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<BindingValue<T>> source)
{
var observer = new DirectBindingObserver<T>(this, property);
@ -212,6 +250,20 @@ namespace Avalonia.PropertyStore
}
}
public void SetLocalValue(AvaloniaProperty property, object? value)
{
if (TryGetEffectiveValue(property, out var existing))
{
existing.SetLocalValueAndRaise(this, property, value);
}
else
{
var effectiveValue = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetLocalValueAndRaise(this, property, value);
}
}
public void SetLocalValue<T>(StyledProperty<T> property, T value)
{
if (TryGetEffectiveValue(property, out var existing))
@ -405,6 +457,7 @@ namespace Avalonia.PropertyStore
/// </summary>
/// <param name="entry">The binding entry.</param>
/// <param name="priority">The priority of binding which produced a new value.</param>
[Obsolete("TODO: Remove?")]
public void OnBindingValueChanged(
IValueEntry entry,
BindingPriority priority)
@ -528,6 +581,7 @@ namespace Avalonia.PropertyStore
/// </summary>
/// <param name="property">The previously bound property.</param>
/// <param name="observer">The observer.</param>
[Obsolete("TODO: Remove?")]
public void OnLocalValueBindingCompleted(AvaloniaProperty property, IDisposable observer)
{
if (_localValueBindings is not null &&
@ -689,6 +743,87 @@ namespace Avalonia.PropertyStore
overridden);
}
void IBindingExpressionSink.OnChanged(
BindingExpression instance,
bool hasValueChanged,
bool hasErrorChanged)
{
Dispatcher.UIThread.VerifyAccess();
Debug.Assert(instance.TargetProperty is not null);
var property = instance.TargetProperty;
var value = instance.GetValueOrDefault();
if (property.IsDirect)
{
if (hasValueChanged)
property.RouteSetDirectValueUnchecked(Owner, value);
}
else
{
var priority = instance.Priority;
if (hasValueChanged)
{
if (priority == BindingPriority.LocalValue)
{
if (value != AvaloniaProperty.UnsetValue)
SetLocalValue(property, value);
else if (property == StyledElement.DataContextProperty)
SetLocalValue(property, null);
else
ClearValue(property);
}
else
{
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.BasePriority)
ReevaluateEffectiveValue(property, existing, changedValueEntry: instance);
}
else
{
AddEffectiveValueAndRaise(property, instance, priority);
}
}
}
}
if (instance.IsDataValidationEnabled)
{
instance.GetDataValidationState(out var state, out var error);
Owner.OnUpdateDataValidation(property, state, error);
}
}
/// <summary>
/// Called by a binding expression when the binding produces completes.
/// </summary>
/// <param name="instance">The binding expression.</param>
void IBindingExpressionSink.OnCompleted(BindingExpression instance)
{
Dispatcher.UIThread.VerifyAccess();
Debug.Assert(instance.TargetProperty is not null);
var property = instance.TargetProperty;
if (instance.IsDataValidationEnabled)
Owner.OnUpdateDataValidation(property, BindingValueType.UnsetValue, null);
if (instance.Priority == BindingPriority.LocalValue)
{
if (_localValueBindings is not null &&
_localValueBindings.TryGetValue(property.Id, out var existing))
{
if (existing == instance)
{
_localValueBindings?.Remove(property.Id);
ClearValue(property);
}
}
}
}
private int InsertFrame(ValueFrame frame)
{
Debug.Assert(!_frames.Contains(frame));

18
src/Avalonia.Base/StyledProperty.cs

@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
@ -160,10 +161,16 @@ namespace Avalonia
bool IStyledPropertyAccessor.ValidateValue(object? value)
{
if (value is null && !typeof(TValue).IsValueType)
return ValidateValue?.Invoke(default!) ?? true;
if (value is TValue typed)
if (value is null)
{
if (!typeof(TValue).IsValueType || Nullable.GetUnderlyingType(typeof(TValue)) != null)
return ValidateValue?.Invoke(default!) ?? true;
}
else if (value is TValue typed)
{
return ValidateValue?.Invoke(typed) ?? true;
}
return false;
}
@ -221,6 +228,11 @@ namespace Avalonia
return target.Bind<TValue>(this, source, priority);
}
internal override IDisposable RouteBind(AvaloniaObject o, BindingExpression source, BindingPriority priority)
{
return o.GetValueStore().AddBinding(this, source, priority);
}
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConversionSupressWarningMessage)]
private bool ShouldSetValue(AvaloniaObject target, object? value, [NotNullWhen(true)] out TValue? converted)
{

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

@ -2,6 +2,7 @@ using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Metadata;
using Avalonia.PropertyStore;
@ -117,7 +118,13 @@ namespace Avalonia.Styling
if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay)
{
return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source);
if (i.Source is BindingExpression expression)
{
expression.Initialize(target.GetValueStore(), Property, instance.Priority);
return expression;
}
else
return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source);
}
throw new NotSupportedException();

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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore;
using Xunit;
@ -174,6 +175,11 @@ namespace Avalonia.Base.UnitTests
throw new NotImplementedException();
}
internal override IDisposable RouteBind(AvaloniaObject o, BindingExpression source, BindingPriority priority)
{
throw new NotImplementedException();
}
internal override void RouteClearValue(AvaloniaObject o)
{
throw new NotImplementedException();

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

@ -105,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { StringValue = "foo" };
var target = BindingExpression.Create(
data,
data,
o => o.StringValue,
fallbackValue: 42,
targetProperty: TargetTypeInt);
@ -126,7 +126,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
var data = new Class1 { StringValue = "foo" };
var target = BindingExpression.Create(
data,
data,
o => o.StringValue,
enableDataValidation: true,
fallbackValue: 42,
@ -253,7 +253,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
new BindingNotification($"{3.4}"),
new BindingNotification(
new InvalidCastException("Could not convert 'bar' (System.String) to System.Double."),
BindingErrorType.DataValidationError)
BindingErrorType.DataValidationError, $"{3.4}")
},
result);

6
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_DataValidation.cs

@ -92,9 +92,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
Assert.Equal(new[]
{
new BindingNotification(0),
// Value is notified twice as ErrorsChanged is always called by IndeiTest.
new BindingNotification(5),
new BindingNotification(5),
// Value is first signalled without an error as validation hasn't been updated.
@ -104,7 +101,8 @@ namespace Avalonia.Base.UnitTests.Data.Core
// Exception is thrown by trying to set value to "foo".
new BindingNotification(
new InvalidCastException("Could not convert 'foo' (System.String) to System.Int32."),
BindingErrorType.DataValidationError),
BindingErrorType.DataValidationError,
-5),
// Value is set then validation is updated.
new BindingNotification(new DataValidationException("Must be positive"), BindingErrorType.DataValidationError, 5),

4
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests_Task.cs

@ -87,7 +87,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
new[]
{
new BindingNotification(
new AggregateException(new NotSupportedException()),
new BindingChainException("Specified method is not supported.", "Foo^", "^"),
BindingErrorType.Error)
},
result);
@ -111,7 +111,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
new[]
{
new BindingNotification(
new AggregateException(new NotSupportedException()),
new BindingChainException("Specified method is not supported.", "Foo^", "^"),
BindingErrorType.Error)
},
result);

Loading…
Cancel
Save