Browse Source

Merge branch 'master' into assetinclude-intrinsic

pull/9413/head
Jumar Macato 3 years ago
committed by GitHub
parent
commit
2a215d04c9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Avalonia.Base/Animation/Animatable.cs
  2. 2
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  3. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  4. 542
      src/Avalonia.Base/AvaloniaObject.cs
  5. 36
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  6. 15
      src/Avalonia.Base/AvaloniaProperty.cs
  7. 24
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  8. 28
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  9. 32
      src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs
  10. 20
      src/Avalonia.Base/Data/BindingNotification.cs
  11. 5
      src/Avalonia.Base/Data/BindingPriority.cs
  12. 130
      src/Avalonia.Base/Data/BindingValue.cs
  13. 9
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  14. 50
      src/Avalonia.Base/DirectPropertyBase.cs
  15. 7
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  16. 25
      src/Avalonia.Base/PropertyStore/AvaloniaPropertyDictionaryPool.cs
  17. 154
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  18. 148
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  19. 82
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  20. 76
      src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
  21. 55
      src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
  22. 168
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  23. 270
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  24. 8
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  25. 18
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  26. 28
      src/Avalonia.Base/PropertyStore/IValue.cs
  27. 30
      src/Avalonia.Base/PropertyStore/IValueEntry.cs
  28. 16
      src/Avalonia.Base/PropertyStore/IValueEntry`1.cs
  29. 31
      src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
  30. 63
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  31. 59
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  32. 41
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  33. 62
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  34. 82
      src/Avalonia.Base/PropertyStore/LoggingUtils.cs
  35. 326
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  36. 35
      src/Avalonia.Base/PropertyStore/PropertyNotifying.cs
  37. 35
      src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
  38. 52
      src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
  39. 33
      src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs
  40. 55
      src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs
  41. 103
      src/Avalonia.Base/PropertyStore/ValueFrame.cs
  42. 45
      src/Avalonia.Base/PropertyStore/ValueOwner.cs
  43. 962
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  44. 59
      src/Avalonia.Base/Reactive/BindingValueAdapter.cs
  45. 33
      src/Avalonia.Base/Reactive/BindingValueExtensions.cs
  46. 6
      src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs
  47. 62
      src/Avalonia.Base/Reactive/TypedBindingAdapter.cs
  48. 55
      src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs
  49. 149
      src/Avalonia.Base/StyledElement.cs
  50. 86
      src/Avalonia.Base/StyledPropertyBase.cs
  51. 42
      src/Avalonia.Base/Styling/Activators/AndActivator.cs
  52. 28
      src/Avalonia.Base/Styling/Activators/IStyleActivator.cs
  53. 3
      src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs
  54. 9
      src/Avalonia.Base/Styling/Activators/NotActivator.cs
  55. 19
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  56. 36
      src/Avalonia.Base/Styling/Activators/OrActivator.cs
  57. 10
      src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs
  58. 65
      src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs
  59. 25
      src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
  60. 4
      src/Avalonia.Base/Styling/ChildSelector.cs
  61. 9
      src/Avalonia.Base/Styling/ControlTheme.cs
  62. 4
      src/Avalonia.Base/Styling/DescendentSelector.cs
  63. 6
      src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs
  64. 12
      src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs
  65. 6
      src/Avalonia.Base/Styling/ISetter.cs
  66. 34
      src/Avalonia.Base/Styling/ISetterInstance.cs
  67. 19
      src/Avalonia.Base/Styling/IStyleInstance.cs
  68. 20
      src/Avalonia.Base/Styling/IStyleable.cs
  69. 2
      src/Avalonia.Base/Styling/NestingSelector.cs
  70. 4
      src/Avalonia.Base/Styling/NotSelector.cs
  71. 4
      src/Avalonia.Base/Styling/NthChildSelector.cs
  72. 5
      src/Avalonia.Base/Styling/OrSelector.cs
  73. 4
      src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
  74. 196
      src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs
  75. 121
      src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
  76. 9
      src/Avalonia.Base/Styling/Selector.cs
  77. 74
      src/Avalonia.Base/Styling/Setter.cs
  78. 15
      src/Avalonia.Base/Styling/Style.cs
  79. 45
      src/Avalonia.Base/Styling/StyleBase.cs
  80. 162
      src/Avalonia.Base/Styling/StyleInstance.cs
  81. 4
      src/Avalonia.Base/Styling/TemplateSelector.cs
  82. 8
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  83. 368
      src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs
  84. 173
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  85. 507
      src/Avalonia.Base/ValueStore.cs
  86. 33
      src/Avalonia.Base/Visual.cs
  87. 64
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  88. 1
      src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml
  89. 75
      tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
  90. 695
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  91. 559
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  92. 120
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs
  93. 31
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs
  94. 213
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs
  95. 61
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs
  96. 28
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
  97. 12
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  98. 27
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  99. 314
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  100. 142
      tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs

2
src/Avalonia.Base/Animation/Animatable.cs

@ -235,7 +235,7 @@ namespace Avalonia.Animation
private object? GetAnimationBaseValue(AvaloniaProperty property)
{
var value = this.GetBaseValue(property, BindingPriority.LocalValue);
var value = this.GetBaseValue(property);
if (value == AvaloniaProperty.UnsetValue)
{

2
src/Avalonia.Base/Animation/AnimationInstance`1.cs

@ -229,7 +229,7 @@ namespace Avalonia.Animation
private void UpdateNeutralValue()
{
var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified.");
var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue);
var baseValue = _targetControl.GetBaseValue(property);
_neutralValue = baseValue != AvaloniaProperty.UnsetValue ?
(T)baseValue! : (T)_targetControl.GetValue(property)!;

2
src/Avalonia.Base/Avalonia.Base.csproj

@ -28,6 +28,7 @@
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" />
@ -39,6 +40,7 @@
<InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />

542
src/Avalonia.Base/AvaloniaObject.cs

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading;
namespace Avalonia
@ -18,13 +18,11 @@ namespace Avalonia
/// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
{
private readonly ValueStore _values;
private AvaloniaObject? _inheritanceParent;
private List<IDisposable>? _directBindings;
private PropertyChangedEventHandler? _inpcChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChanged;
private List<AvaloniaObject>? _inheritanceChildren;
private ValueStore? _values;
private bool _batchUpdate;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@ -32,6 +30,7 @@ namespace Avalonia
public AvaloniaObject()
{
VerifyAccess();
_values = new ValueStore(this);
}
/// <summary>
@ -59,7 +58,7 @@ namespace Avalonia
/// <value>
/// The inheritance parent.
/// </value>
protected AvaloniaObject? InheritanceParent
protected internal AvaloniaObject? InheritanceParent
{
get
{
@ -72,28 +71,10 @@ namespace Avalonia
if (_inheritanceParent != value)
{
var oldParent = _inheritanceParent;
var valuestore = _values;
_inheritanceParent?.RemoveInheritanceChild(this);
_inheritanceParent = value;
var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType());
var propertiesCount = properties.Count;
for (var i = 0; i < propertiesCount; i++)
{
var property = properties[i];
if (valuestore?.IsSet(property) == true)
{
// If local value set there can be no change.
continue;
}
property.RouteInheritanceParentChanged(this, oldParent);
}
_inheritanceParent?.AddInheritanceChild(this);
_values.SetInheritanceParent(value);
}
}
}
@ -118,24 +99,15 @@ namespace Avalonia
set { this.Bind(binding.Property!, value); }
}
private ValueStore Values
{
get
{
if (_values is null)
{
_values = new ValueStore(this);
if (_batchUpdate)
_values.BeginBatchUpdate();
}
return _values;
}
}
/// <summary>
/// Returns a value indicating whether the current thread is the UI thread.
/// </summary>
/// <returns>true if the current thread is the UI thread; otherwise false.</returns>
public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
/// <summary>
/// Checks that the current thread is the UI thread and throws if not.
/// </summary>
public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
/// <summary>
@ -144,9 +116,9 @@ namespace Avalonia
/// <param name="property">The property.</param>
public void ClearValue(AvaloniaProperty property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
property.RouteClearValue(this);
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
_values.ClearLocalValue(property);
}
/// <summary>
@ -234,9 +206,12 @@ namespace Avalonia
/// <returns>The value.</returns>
public object? GetValue(AvaloniaProperty property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
_ = property ?? throw new ArgumentNullException(nameof(property));
return property.RouteGetValue(this);
if (property.IsDirect)
return property.RouteGetValue(this);
else
return _values.GetValue(property);
}
/// <summary>
@ -247,10 +222,9 @@ namespace Avalonia
/// <returns>The value.</returns>
public T GetValue<T>(StyledPropertyBase<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
return GetValueOrInheritedOrDefault(property);
return _values.GetValue(property);
}
/// <summary>
@ -269,18 +243,11 @@ namespace Avalonia
}
/// <inheritdoc/>
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property, BindingPriority maxPriority)
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
if (_values is object &&
_values.TryGetValue(property, maxPriority, out var value))
{
return value;
}
return default;
return _values.GetBaseValue(property);
}
/// <summary>
@ -346,26 +313,20 @@ namespace Avalonia
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
ValidatePriority(priority);
LogPropertySet(property, value, priority);
LogPropertySet(property, value, BindingPriority.LocalValue);
if (value is UnsetValueType)
{
if (priority == BindingPriority.LocalValue)
{
Values.ClearLocalValue(property);
}
else
{
throw new NotSupportedException(
"Cannot set property to Unset at non-local value priority.");
}
_values.ClearLocalValue(property);
}
else if (!(value is DoNothingType))
else if (value is not DoNothingType)
{
return Values.SetValue(property, value, priority);
return _values.SetValue(property, value, priority);
}
return null;
@ -382,6 +343,7 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
LogPropertySet(property, value, BindingPriority.LocalValue);
SetDirectValueUnchecked(property, value);
}
@ -398,12 +360,52 @@ namespace Avalonia
public IDisposable Bind(
AvaloniaProperty property,
IObservable<object?> source,
BindingPriority priority = BindingPriority.LocalValue) => property.RouteBind(this, source, priority);
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
StyledPropertyBase<T> property,
IObservable<object?> source,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess();
ValidatePriority(priority);
return _values.AddBinding(property, source, priority);
}
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
StyledPropertyBase<T> property,
IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess();
ValidatePriority(priority);
return property.RouteBind(this, source.ToBindingValue(), priority);
return _values.AddBinding(property, source, priority);
}
/// <summary>
@ -424,8 +426,9 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess();
ValidatePriority(priority);
return Values.AddBinding(property, source, priority);
return _values.AddBinding(property, source, priority);
}
/// <summary>
@ -439,10 +442,9 @@ namespace Avalonia
/// </returns>
public IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source)
IObservable<object?> source)
{
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -452,48 +454,67 @@ namespace Avalonia
throw new ArgumentException($"The property {property.Name} is readonly.");
}
Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log(
this,
"Bound {Property} to {Binding} with priority LocalValue",
property,
GetDescription(source));
_directBindings ??= new List<IDisposable>();
return new DirectBindingSubscription<T>(this, property, source);
return _values.AddBinding(property, source);
}
/// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>.
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
public void CoerceValue(AvaloniaProperty property)
/// <param name="source">The observable.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<T> source)
{
_values?.CoerceValue(property);
}
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
public void BeginBatchUpdate()
{
if (_batchUpdate)
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
if (property.IsReadOnly)
{
throw new InvalidOperationException("Batch update already in progress.");
throw new ArgumentException($"The property {property.Name} is readonly.");
}
_batchUpdate = true;
_values?.BeginBatchUpdate();
return _values.AddBinding(property, source);
}
public void EndBatchUpdate()
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
if (!_batchUpdate)
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
if (property.IsReadOnly)
{
throw new InvalidOperationException("No batch update in progress.");
throw new ArgumentException($"The property {property.Name} is readonly.");
}
_batchUpdate = false;
_values?.EndBatchUpdate();
return _values.AddBinding(property, source);
}
/// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="property">The property.</param>
public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property);
/// <inheritdoc/>
internal void AddInheritanceChild(AvaloniaObject child)
{
@ -507,98 +528,12 @@ namespace Avalonia
_inheritanceChildren?.Remove(child);
}
internal void InheritedPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
Optional<T> newValue)
{
if (property.Inherits && (_values == null || !_values.IsSet(property)))
{
RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
}
}
/// <inheritdoc/>
Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
{
return _propertyChanged?.GetInvocationList();
}
internal void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
var property = (StyledPropertyBase<T>)change.Property;
LogIfError(property, change.NewValue);
// If the change is to the effective value of the property and no old/new value is set
// then fill in the old/new value from property inheritance/default value. We don't do
// this for non-effective value changes because these are only needed for property
// transitions, where knowing e.g. that an inherited value is active at an arbitrary
// priority isn't of any use and would introduce overhead.
if (change.IsEffectiveValueChange && !change.OldValue.HasValue)
{
change.SetOldValue(GetInheritedOrDefault<T>(property));
}
if (change.IsEffectiveValueChange && !change.NewValue.HasValue)
{
change.SetNewValue(GetInheritedOrDefault(property));
}
if (!change.IsEffectiveValueChange ||
!EqualityComparer<T>.Default.Equals(change.OldValue.Value, change.NewValue.Value))
{
RaisePropertyChanged(change);
if (change.IsEffectiveValueChange)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log(
this,
"{Property} changed from {$Old} to {$Value} with priority {Priority}",
property,
change.OldValue,
change.NewValue,
change.Priority);
}
}
}
internal void Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue)
{
var change = new AvaloniaPropertyChangedEventArgs<T>(
this,
property,
oldValue,
default,
BindingPriority.Unset);
ValueChanged(change);
}
/// <summary>
/// Called for each inherited property when the <see cref="InheritanceParent"/> changes.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="property">The property.</param>
/// <param name="oldParent">The old inheritance parent.</param>
internal void InheritanceParentChanged<T>(
StyledPropertyBase<T> property,
AvaloniaObject? oldParent)
{
var oldValue = oldParent is not null ?
oldParent.GetValueOrInheritedOrDefault(property) :
property.GetDefaultValue(GetType());
var newValue = GetInheritedOrDefault(property);
if (!EqualityComparer<T>.Default.Equals(oldValue, newValue))
{
RaisePropertyChanged(property, oldValue, newValue);
}
}
internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property)
{
if (property.IsDirect)
@ -626,19 +561,23 @@ namespace Avalonia
"Unset");
}
internal ValueStore GetValueStore() => _values;
internal IReadOnlyList<AvaloniaObject>? GetInheritanceChildren() => _inheritanceChildren;
/// <summary>
/// Logs a binding error for a property.
/// Gets a logger to which a binding warning may be written.
/// </summary>
/// <param name="property">The property that the error occurred on.</param>
/// <param name="e">The binding error.</param>
protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e)
/// <param name="e">The binding exception, if any.</param>
/// <remarks>
/// This is overridden in <see cref="Visual"/> to prevent logging binding errors when a
/// control is not attached to the visual tree.
/// </remarks>
internal virtual ParametrizedLogger? GetBindingWarningLogger(
AvaloniaProperty property,
Exception? e)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log(
this,
"Error in binding to {Target}.{Property}: {Message}",
this,
property,
e.Message);
return Logger.TryGet(LogEventLevel.Warning, LogArea.Binding);
}
/// <summary>
@ -675,6 +614,22 @@ namespace Avalonia
{
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event for a direct property.
/// </summary>
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
private protected void RaisePropertyChanged<T>(
DirectPropertyBase<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue)
{
RaisePropertyChanged(property, oldValue, newValue, priority, true);
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event.
/// </summary>
@ -682,18 +637,32 @@ namespace Avalonia
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected internal void RaisePropertyChanged<T>(
/// <param name="isEffectiveValue">
/// Whether the notification represents a change to the effective value of the property.
/// </param>
internal void RaisePropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue)
BindingPriority priority,
bool isEffectiveValue)
{
RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs<T>(
var e = new AvaloniaPropertyChangedEventArgs<T>(
this,
property,
oldValue,
newValue,
priority));
priority,
isEffectiveValue);
OnPropertyChangedCore(e);
if (isEffectiveValue)
{
property.NotifyChanged(e);
_propertyChanged?.Invoke(this, e);
_inpcChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name));
}
}
/// <summary>
@ -718,110 +687,24 @@ namespace Avalonia
var old = field;
field = value;
RaisePropertyChanged(property, old, value);
RaisePropertyChanged(property, old, value, BindingPriority.LocalValue, true);
return true;
}
private T GetInheritedOrDefault<T>(StyledPropertyBase<T> property)
{
if (property.Inherits && InheritanceParent is AvaloniaObject o)
{
return o.GetValueOrInheritedOrDefault(property);
}
return property.GetDefaultValue(GetType());
}
private T GetValueOrInheritedOrDefault<T>(
StyledPropertyBase<T> property,
BindingPriority maxPriority = BindingPriority.Animation)
{
var o = this;
var inherits = property.Inherits;
var value = default(T);
while (o != null)
{
var values = o._values;
if (values != null
&& values.TryGetValue(property, maxPriority, out value) == true)
{
return value;
}
if (!inherits)
{
break;
}
o = o.InheritanceParent as AvaloniaObject;
}
return property.GetDefaultValue(GetType());
}
protected internal void RaisePropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
VerifyAccess();
if (change.IsEffectiveValueChange)
{
change.Property.Notifying?.Invoke(this, true);
}
try
{
OnPropertyChangedCore(change);
if (change.IsEffectiveValueChange)
{
change.Property.NotifyChanged(change);
_propertyChanged?.Invoke(this, change);
if (_inpcChanged != null)
{
var inpce = new PropertyChangedEventArgs(change.Property.Name);
_inpcChanged(this, inpce);
}
if (change.Property.Inherits && _inheritanceChildren != null)
{
foreach (var child in _inheritanceChildren)
{
child.InheritedPropertyChanged(
change.Property,
change.OldValue,
change.NewValue.ToOptional());
}
}
}
}
finally
{
if (change.IsEffectiveValueChange)
{
change.Property.Notifying?.Invoke(this, false);
}
}
}
/// <summary>
/// Sets the value of a direct property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, T value)
internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, T value)
{
var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
if (value is UnsetValueType)
{
p.InvokeSetter(this, p.GetUnsetValue(GetType()));
property.InvokeSetter(this, property.GetUnsetValue(GetType()));
}
else if (!(value is DoNothingType))
{
p.InvokeSetter(this, value);
property.InvokeSetter(this, value);
}
}
@ -830,16 +713,9 @@ namespace Avalonia
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
{
var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property);
if (p == null)
{
throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
}
LogIfError(property, value);
LoggingUtils.LogIfNecessary(this, property, value);
switch (value.Type)
{
@ -858,7 +734,7 @@ namespace Avalonia
break;
}
var metadata = p.GetMetadata(GetType());
var metadata = property.GetMetadata(GetType());
if (metadata.EnableDataValidation == true)
{
@ -877,29 +753,6 @@ namespace Avalonia
return description?.Description ?? o.ToString() ?? o.GetType().Name;
}
/// <summary>
/// Logs a message if the notification represents a binding error.
/// </summary>
/// <param name="property">The property being bound.</param>
/// <param name="value">The binding notification.</param>
private void LogIfError<T>(AvaloniaProperty property, BindingValue<T> value)
{
if (value.HasError)
{
if (value.Error is AggregateException aggregate)
{
foreach (var inner in aggregate.InnerExceptions)
{
LogBindingError(property, inner);
}
}
else
{
LogBindingError(property, value.Error!);
}
}
}
/// <summary>
/// Logs a property set message.
/// </summary>
@ -916,49 +769,16 @@ namespace Avalonia
priority);
}
private class DirectBindingSubscription<T> : IObserver<BindingValue<T>>, IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidatePriority(BindingPriority priority)
{
private readonly AvaloniaObject _owner;
private readonly DirectPropertyBase<T> _property;
private readonly IDisposable _subscription;
public DirectBindingSubscription(
AvaloniaObject owner,
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
_owner = owner;
_property = property;
_owner._directBindings!.Add(this);
_subscription = source.Subscribe(this);
}
public void Dispose()
{
// _subscription can be null, if Subscribe failed with an exception.
_subscription?.Dispose();
_owner._directBindings!.Remove(this);
}
public void OnCompleted() => Dispose();
public void OnError(Exception error) => Dispose();
public void OnNext(BindingValue<T> value)
{
if (Dispatcher.UIThread.CheckAccess())
{
_owner.SetDirectValueUnchecked(_property, value);
}
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 instance = _owner;
var property = _property;
var newValue = value;
if (priority < BindingPriority.Animation || priority >= BindingPriority.Inherited)
ThrowInvalidPriority(priority);
}
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue));
}
}
private static void ThrowInvalidPriority(BindingPriority priority)
{
throw new ArgumentException($"Invalid priority ${priority}", nameof(priority));
}
}
}

36
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -261,7 +261,6 @@ namespace Avalonia
}
throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
}
/// <summary>
@ -280,14 +279,17 @@ namespace Avalonia
IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue)
{
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
if (target is AvaloniaObject ao)
{
return property switch
{
StyledPropertyBase<T> styled => ao.Bind(styled, source, priority),
DirectPropertyBase<T> direct => ao.Bind(direct, source),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
};
}
return target.Bind(
property,
source.ToBindingValue(),
priority);
throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
}
/// <summary>
@ -362,10 +364,8 @@ namespace Avalonia
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks>
/// For styled properties, gets the value of the property if set on the object with a
/// priority equal or lower to <paramref name="maxPriority"/>, otherwise
/// For styled properties, gets the value of the property excluding animated values, otherwise
/// <see cref="AvaloniaProperty.UnsetValue"/>. Note that this method does not return
/// property values that come from inherited or default values.
///
@ -373,14 +373,13 @@ namespace Avalonia
/// </remarks>
public static object? GetBaseValue(
this IAvaloniaObject target,
AvaloniaProperty property,
BindingPriority maxPriority)
AvaloniaProperty property)
{
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
if (target is AvaloniaObject ao)
return property.RouteGetBaseValue(ao, maxPriority);
return property.RouteGetBaseValue(ao);
throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
}
@ -389,10 +388,8 @@ namespace Avalonia
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks>
/// For styled properties, gets the value of the property if set on the object with a
/// priority equal or lower to <paramref name="maxPriority"/>, otherwise
/// For styled properties, gets the value of the property excluding animated values, otherwise
/// <see cref="Optional{T}.Empty"/>. Note that this method does not return property values
/// that come from inherited or default values.
///
@ -400,8 +397,7 @@ namespace Avalonia
/// </remarks>
public static Optional<T> GetBaseValue<T>(
this IAvaloniaObject target,
AvaloniaProperty<T> property,
BindingPriority maxPriority)
AvaloniaProperty<T> property)
{
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
@ -410,7 +406,7 @@ namespace Avalonia
{
return property switch
{
StyledPropertyBase<T> styled => ao.GetBaseValue(styled, maxPriority),
StyledPropertyBase<T> styled => ao.GetBaseValue(styled),
DirectPropertyBase<T> direct => ao.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};

15
src/Avalonia.Base/AvaloniaProperty.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.Utilities;
@ -455,6 +456,12 @@ namespace Avalonia
return Name;
}
/// <summary>
/// Creates an effective value for the property.
/// </summary>
/// <param name="o">The effective value owner.</param>
internal abstract EffectiveValue CreateEffectiveValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped ClearValue call to a typed call.
/// </summary>
@ -471,8 +478,7 @@ namespace Avalonia
/// Routes an untyped GetBaseValue call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
internal abstract object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority);
internal abstract object? RouteGetBaseValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped SetValue call to a typed call.
@ -496,12 +502,9 @@ namespace Avalonia
/// <param name="priority">The priority.</param>
internal abstract IDisposable RouteBind(
AvaloniaObject o,
IObservable<BindingValue<object?>> source,
IObservable<object?> source,
BindingPriority priority);
internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent);
internal abstract ISetterInstance CreateSetterInstance(IStyleable target, object? value);
/// <summary>
/// Overrides the metadata for the property on the specified type.
/// </summary>

24
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -17,6 +17,16 @@ namespace Avalonia
IsEffectiveValueChange = true;
}
internal AvaloniaPropertyChangedEventArgs(
IAvaloniaObject sender,
BindingPriority priority,
bool isEffectiveValueChange)
{
Sender = sender;
Priority = priority;
IsEffectiveValueChange = isEffectiveValueChange;
}
/// <summary>
/// Gets the <see cref="AvaloniaObject"/> that the property changed on.
/// </summary>
@ -49,20 +59,8 @@ namespace Avalonia
/// </value>
public BindingPriority Priority { get; private set; }
/// <summary>
/// Gets a value indicating whether the change represents a change to the effective value of
/// the property.
/// </summary>
/// <remarks>
/// This will usually be true, except in
/// <see cref="AvaloniaObject.OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs)"/>
/// which receives notifications for all changes to property values, whether a value with a higher
/// priority is present or not. When this property is false, the change that is being signaled
/// has not resulted in a change to the property value on the object.
/// </remarks>
public bool IsEffectiveValueChange { get; private set; }
internal bool IsEffectiveValueChange { get; private set; }
internal void MarkNonEffectiveValue() => IsEffectiveValueChange = false;
protected abstract AvaloniaProperty GetProperty();
protected abstract object? GetOldValue();
protected abstract object? GetNewValue();

28
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs

@ -21,7 +21,18 @@ namespace Avalonia
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
: base(sender, priority)
: this(sender, property, oldValue, newValue, priority, true)
{
}
internal AvaloniaPropertyChangedEventArgs(
IAvaloniaObject sender,
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority,
bool isEffectiveValueChange)
: base(sender, priority, isEffectiveValueChange)
{
Property = property;
OldValue = oldValue;
@ -39,28 +50,13 @@ namespace Avalonia
/// <summary>
/// Gets the old value of the property.
/// </summary>
/// <remarks>
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// old value of the property on the object.
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false, returns
/// <see cref="Optional{T}.Empty"/>.
/// </remarks>
public new Optional<T> OldValue { get; private set; }
/// <summary>
/// Gets the new value of the property.
/// </summary>
/// <remarks>
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is true, returns the
/// value of the property on the object.
/// When <see cref="AvaloniaPropertyChangedEventArgs.IsEffectiveValueChange"/> is false returns the
/// changed value, or <see cref="Optional{T}.Empty"/> if the value was removed.
/// </remarks>
public new BindingValue<T> NewValue { get; private set; }
internal void SetOldValue(Optional<T> value) => OldValue = value;
internal void SetNewValue(BindingValue<T> value) => NewValue = value;
protected override AvaloniaProperty GetProperty() => Property;
protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue);

32
src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace System;
#if !NET6_0_OR_GREATER
internal static class CollectionCompatibilityExtensions
{
public static bool Remove<TKey, TValue>(
this Dictionary<TKey, TValue> o,
TKey key,
[MaybeNullWhen(false)] out TValue value)
where TKey : notnull
{
if (o.TryGetValue(key, out value))
return o.Remove(key);
return false;
}
public static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> o, TKey key, TValue value)
where TKey : notnull
{
if (!o.ContainsKey(key))
{
o.Add(key, value);
return true;
}
return false;
}
}
#endif

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

@ -241,26 +241,6 @@ namespace Avalonia.Data
_value = value;
}
public BindingValue<object?> ToBindingValue()
{
if (ErrorType == BindingErrorType.None)
{
return HasValue ? new BindingValue<object?>(Value) : BindingValue<object?>.Unset;
}
else if (ErrorType == BindingErrorType.Error)
{
return BindingValue<object?>.BindingError(
Error!,
HasValue ? new Optional<object?>(Value) : Optional<object?>.Empty);
}
else
{
return BindingValue<object?>.DataValidationError(
Error!,
HasValue ? new Optional<object?>(Value) : Optional<object?>.Empty);
}
}
/// <inheritdoc/>
public override string ToString()
{

5
src/Avalonia.Base/Data/BindingPriority.cs

@ -35,6 +35,11 @@ namespace Avalonia.Data
/// A style binding.
/// </summary>
Style,
/// <summary>
/// The value is inherited from an ancestor element.
/// </summary>
Inherited,
/// <summary>
/// The binding is uninitialized.

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Utilities;
@ -230,19 +231,77 @@ namespace Avalonia.Data
/// <summary>
/// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
/// <see cref="AvaloniaProperty.UnsetValue"/> and <see cref="BindingOperations.DoNothing"/>.
/// <see cref="AvaloniaProperty.UnsetValue"/>, <see cref="BindingOperations.DoNothing"/> and
/// <see cref="BindingNotification"/>.
/// </summary>
/// <param name="value">The untyped value.</param>
/// <returns>The typed binding value.</returns>
public static BindingValue<T> FromUntyped(object? value)
{
return value switch
return FromUntyped(value, typeof(T));
}
/// <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"/>.
/// </summary>
/// <param name="value">The untyped value.</param>
/// <param name="targetType">The runtime target type.</param>
/// <returns>The typed binding value.</returns>
public static BindingValue<T> FromUntyped(object? value, Type targetType)
{
if (value == AvaloniaProperty.UnsetValue)
return Unset;
else if (value == BindingOperations.DoNothing)
return DoNothing;
var type = BindingValueType.Value;
T? v = default;
Exception? error = null;
List<Exception>? errors = null;
if (value is BindingNotification n)
{
UnsetValueType _ => Unset,
DoNothingType _ => DoNothing,
BindingNotification n => n.ToBindingValue().Cast<T>(),
_ => new BindingValue<T>((T)value!)
};
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)
{
if (TypeUtilities.TryConvertImplicit(targetType, value, out var typed))
v = (T)typed!;
else
{
var e = new InvalidCastException(
$"Unable to convert object '{value ?? "(null)"}' " +
$"of type '{value?.GetType()}' to type '{targetType}'.");
if (error is null)
error = e;
else
{
errors ??= new List<Exception>() { error };
errors.Add(e);
}
type = BindingValueType.BindingError;
}
}
if (errors is not null)
error = new AggregateException(errors);
return new BindingValue<T>(type, v, error);
}
/// <summary>
@ -372,61 +431,4 @@ namespace Avalonia.Data
}
}
}
public static class BindingValueExtensions
{
/// <summary>
/// Casts the type of a <see cref="BindingValue{T}"/> using only the C# cast operator.
/// </summary>
/// <typeparam name="T">The target type.</typeparam>
/// <param name="value">The binding value.</param>
/// <returns>The cast value.</returns>
public static BindingValue<T> Cast<T>(this BindingValue<object?> value)
{
return value.Type switch
{
BindingValueType.DoNothing => BindingValue<T>.DoNothing,
BindingValueType.UnsetValue => BindingValue<T>.Unset,
BindingValueType.Value => new BindingValue<T>((T)value.Value!),
BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!),
BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError(
value.Error!,
(T)value.Value!),
BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!),
BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError(
value.Error!,
(T)value.Value!),
_ => throw new NotSupportedException("Invalid BindingValue type."),
};
}
/// <summary>
/// Casts the type of a <see cref="BindingValue{T}"/> using the implicit conversions
/// allowed by the C# language.
/// </summary>
/// <typeparam name="T">The target type.</typeparam>
/// <param name="value">The binding value.</param>
/// <returns>The cast value.</returns>
/// <remarks>
/// Note that this method uses reflection and as such may be slow.
/// </remarks>
public static BindingValue<T> Convert<T>(this BindingValue<object?> value)
{
return value.Type switch
{
BindingValueType.DoNothing => BindingValue<T>.DoNothing,
BindingValueType.UnsetValue => BindingValue<T>.Unset,
BindingValueType.Value => new BindingValue<T>(TypeUtilities.ConvertImplicit<T>(value.Value!)),
BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!),
BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError(
value.Error!,
TypeUtilities.ConvertImplicit<T>(value.Value!)),
BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!),
BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError(
value.Error!,
TypeUtilities.ConvertImplicit<T>(value.Value!)),
_ => throw new NotSupportedException("Invalid BindingValue type."),
};
}
}
}

9
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -52,6 +52,7 @@ namespace Avalonia.Data.Core
private static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node;
private object? _root;
private Func<object?>? _rootGetter;
private IDisposable? _rootSubscription;
private WeakReference<object?>? _value;
private IReadOnlyList<ITransformNode>? _transformNodes;
@ -109,11 +110,9 @@ namespace Avalonia.Data.Core
IObservable<Unit> update,
string? description)
{
_ = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter));
Description = description;
_node = node ?? throw new ArgumentNullException(nameof(rootGetter));
_node.Target = new WeakReference<object?>(rootGetter());
_rootGetter = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter));
_node = node ?? throw new ArgumentNullException(nameof(node));
_root = update.Select(x => rootGetter());
}
@ -263,6 +262,8 @@ namespace Avalonia.Data.Core
protected override void Initialize()
{
_value = null;
if (_rootGetter is not null)
_node.Target = new WeakReference<object?>(_rootGetter());
_node.Subscribe(ValueChanged);
StartRoot();
}

50
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Styling;
@ -105,6 +106,11 @@ namespace Avalonia
base.OverrideMetadata(type, metadata);
}
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{
throw new InvalidOperationException("Cannot create EffectiveValue for direct property.");
}
/// <inheritdoc/>
internal override void RouteClearValue(AvaloniaObject o)
{
@ -117,7 +123,7 @@ namespace Avalonia
return o.GetValue<TValue>(this);
}
internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority)
internal override object? RouteGetBaseValue(AvaloniaObject o)
{
return o.GetValue<TValue>(this);
}
@ -146,44 +152,18 @@ namespace Avalonia
return null;
}
/// <inheritdoc/>
/// <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 override IDisposable RouteBind(
AvaloniaObject o,
IObservable<BindingValue<object?>> source,
IObservable<object?> source,
BindingPriority priority)
{
var adapter = TypedBindingAdapter<TValue>.Create(o, this, source);
return o.Bind<TValue>(this, adapter);
}
internal override void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent)
{
throw new NotSupportedException("Direct properties do not support inheritance.");
}
internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value)
{
if (value is IBinding binding)
{
return new PropertySetterBindingInstance<TValue>(
target,
this,
binding);
}
else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType))
{
return new PropertySetterTemplateInstance<TValue>(
target,
this,
template);
}
else
{
return new PropertySetterInstance<TValue>(
target,
this,
(TValue)value!);
}
return o.Bind(this, source);
}
}
}

7
src/Avalonia.Base/IStyledPropertyAccessor.cs

@ -15,5 +15,12 @@ namespace Avalonia
/// The default value.
/// </returns>
object? GetDefaultValue(Type type);
/// <summary>
/// Validates the specified property value.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>True if the value is valid, otherwise false.</returns>
bool ValidateValue(object? value);
}
}

25
src/Avalonia.Base/PropertyStore/AvaloniaPropertyDictionaryPool.cs

@ -0,0 +1,25 @@
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore
{
internal static class AvaloniaPropertyDictionaryPool<TValue>
{
private const int MaxPoolSize = 4;
private static readonly Stack<AvaloniaPropertyDictionary<TValue>> _pool = new();
public static AvaloniaPropertyDictionary<TValue> Get()
{
return _pool.Count == 0 ? new() : _pool.Pop();
}
public static void Release(AvaloniaPropertyDictionary<TValue> dictionary)
{
if (_pool.Count < MaxPoolSize)
{
dictionary.Clear();
_pool.Push(dictionary);
}
}
}
}

154
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -1,154 +0,0 @@
using System;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
/// </summary>
internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
{
void Start(bool ignoreBatchUpdate);
}
/// <summary>
/// Stores a binding in a <see cref="ValueStore"/> or <see cref="PriorityValue{T}"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserver<BindingValue<T>>
{
private readonly AvaloniaObject _owner;
private ValueOwner<T> _sink;
private IDisposable? _subscription;
private bool _isSubscribed;
private bool _batchUpdate;
private Optional<T> _value;
public BindingEntry(
AvaloniaObject owner,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority,
ValueOwner<T> sink)
{
_owner = owner;
Property = property;
Source = source;
Priority = priority;
_sink = sink;
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; private set; }
public IObservable<BindingValue<T>> Source { get; }
Optional<object?> IValue.GetValue() => _value.ToObject();
public void BeginBatchUpdate() => _batchUpdate = true;
public void EndBatchUpdate()
{
_batchUpdate = false;
if (_sink.IsValueStore)
Start();
}
public Optional<T> GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
OnCompleted();
}
public void OnCompleted()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_isSubscribed = false;
_sink.Completed(Property, this, oldValue);
}
public void OnError(Exception error)
{
throw new NotImplementedException("BindingEntry.OnError is not implemented", error);
}
public void OnNext(BindingValue<T> value)
{
if (Dispatcher.UIThread.CheckAccess())
{
UpdateValue(value);
}
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 instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue));
}
}
public void Start() => Start(false);
public void Start(bool ignoreBatchUpdate)
{
// We can't use _subscription to check whether we're subscribed because it won't be set
// until Subscribe has finished, which will be too late to prevent reentrancy. In addition
// don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
if (!_isSubscribed &&
Priority != BindingPriority.Unset &&
(!_batchUpdate || ignoreBatchUpdate))
{
_isSubscribed = true;
_subscription = Source.Subscribe(this);
}
}
public void Reparent(PriorityValue<T> parent) => _sink = new(parent);
public void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
{
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}
private void UpdateValue(BindingValue<T> value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
{
value = Property.GetDefaultValue(_owner.GetType());
}
if (value.Type == BindingValueType.DoNothing)
{
return;
}
var old = _value;
if (value.Type != BindingValueType.DataValidationError)
{
_value = value.ToOptional();
}
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(_owner, Property, old, value, Priority));
}
}
}

148
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal abstract class BindingEntryBase<TValue, TSource> : IValueEntry<TValue>,
IObserver<TSource>,
IObserver<BindingValue<TSource>>,
IDisposable
{
private static readonly IDisposable s_creating = Disposable.Empty;
private static readonly IDisposable s_creatingQuiet = Disposable.Create(() => { });
private IDisposable? _subscription;
private bool _hasValue;
private TValue? _value;
protected BindingEntryBase(
ValueFrame frame,
AvaloniaProperty property,
IObservable<BindingValue<TSource>> source)
{
Frame = frame;
Source = source;
Property = property;
}
protected BindingEntryBase(
ValueFrame frame,
AvaloniaProperty property,
IObservable<TSource> source)
{
Frame = frame;
Source = source;
Property = property;
}
public bool HasValue
{
get
{
Start(produceValue: false);
return _hasValue;
}
}
public bool IsSubscribed => _subscription is not null;
public AvaloniaProperty Property { get; }
AvaloniaProperty IValueEntry.Property => Property;
protected ValueFrame Frame { get; }
protected object Source { get; }
public void Dispose()
{
Unsubscribe();
BindingCompleted();
}
public TValue GetValue()
{
Start(produceValue: false);
if (!_hasValue)
throw new AvaloniaInternalException("The binding entry has no value.");
return _value!;
}
public void Start() => Start(true);
public void OnCompleted() => BindingCompleted();
public void OnError(Exception error) => BindingCompleted();
public void OnNext(TSource value) => SetValue(ConvertAndValidate(value));
public void OnNext(BindingValue<TSource> value) => SetValue(ConvertAndValidate(value));
public virtual void Unsubscribe()
{
_subscription?.Dispose();
_subscription = null;
}
object? IValueEntry.GetValue()
{
Start(produceValue: false);
if (!_hasValue)
throw new AvaloniaInternalException("The BindingEntry<T> has no value.");
return _value!;
}
protected abstract BindingValue<TValue> ConvertAndValidate(TSource value);
protected abstract BindingValue<TValue> ConvertAndValidate(BindingValue<TSource> value);
protected virtual void Start(bool produceValue)
{
if (_subscription is not null)
return;
_subscription = produceValue ? s_creating : s_creatingQuiet;
_subscription = Source switch
{
IObservable<BindingValue<TSource>> bv => bv.Subscribe(this),
IObservable<TSource> b => b.Subscribe(this),
_ => throw new AvaloniaInternalException("Unexpected binding source."),
};
}
private void ClearValue()
{
if (_hasValue)
{
_hasValue = false;
_value = default;
if (_subscription is not null)
Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority);
}
}
private void SetValue(BindingValue<TValue> value)
{
if (Frame.Owner is null)
return;
LoggingUtils.LogIfNecessary(Frame.Owner.Owner, Property, value);
if (value.HasValue)
{
if (!_hasValue || !EqualityComparer<TValue>.Default.Equals(_value, value.Value))
{
_value = value.Value;
_hasValue = true;
if (_subscription is not null && _subscription != s_creatingQuiet)
Frame.Owner?.OnBindingValueChanged(this, Frame.Priority);
}
}
else if (value.Type != BindingValueType.DoNothing)
{
ClearValue();
if (_subscription is not null && _subscription != s_creatingQuiet)
Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority);
}
}
private void BindingCompleted()
{
_subscription = null;
Frame.OnBindingCompleted(this);
}
}
}

82
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -1,82 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
/// </summary>
internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
{
}
/// <summary>
/// Stores a value with a priority in a <see cref="ValueStore"/> or
/// <see cref="PriorityValue{T}"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
{
private ValueOwner<T> _sink;
private Optional<T> _value;
public ConstantValueEntry(
StyledPropertyBase<T> property,
T value,
BindingPriority priority,
ValueOwner<T> sink)
{
Property = property;
_value = value;
Priority = priority;
_sink = sink;
}
public ConstantValueEntry(
StyledPropertyBase<T> property,
Optional<T> value,
BindingPriority priority,
ValueOwner<T> sink)
{
Property = property;
_value = value;
Priority = priority;
_sink = sink;
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; private set; }
Optional<object?> IValue.GetValue() => _value.ToObject();
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public void Dispose()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_sink.Completed(Property, this, oldValue);
}
public void Reparent(PriorityValue<T> sink) => _sink = new(sink);
public void Start() { }
public void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
{
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}
}
}

76
src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs

@ -0,0 +1,76 @@
using System;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
internal class DirectBindingObserver<T> : IObserver<T>,
IObserver<BindingValue<T>>,
IDisposable
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
public DirectBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{
_owner = owner;
Property = property;
}
public DirectPropertyBase<T> Property { get;}
public void Start(IObservable<T> source)
{
_subscription = source.Subscribe(this);
}
public void Start(IObservable<BindingValue<T>> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(T value)
{
if (Dispatcher.UIThread.CheckAccess())
{
_owner.Owner.SetDirectValueUnchecked<T>(Property, value);
}
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 instance = _owner.Owner;
var property = Property;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue));
}
}
public void OnNext(BindingValue<T> value)
{
if (Dispatcher.UIThread.CheckAccess())
{
_owner.Owner.SetDirectValueUnchecked<T>(Property, value);
}
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 instance = _owner.Owner;
var property = Property;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue));
}
}
}
}

55
src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs

@ -0,0 +1,55 @@
using System;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
internal class DirectUntypedBindingObserver<T> : IObserver<object?>,
IDisposable
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{
_owner = owner;
Property = property;
}
public DirectPropertyBase<T> Property { get;}
public void Start(IObservable<object?> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(object? value)
{
var typed = BindingValue<T>.FromUntyped(value);
if (Dispatcher.UIThread.CheckAccess())
{
_owner.Owner.SetDirectValueUnchecked<T>(Property, typed);
}
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 instance = _owner.Owner;
var property = Property;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, typed));
}
}
}
}

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

@ -0,0 +1,168 @@
using System.Diagnostics;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents the active value for a property in a <see cref="ValueStore"/>.
/// </summary>
/// <remarks>
/// This class is an abstract base for the generic <see cref="EffectiveValue{T}"/>.
/// </remarks>
internal abstract class EffectiveValue
{
private IValueEntry? _valueEntry;
private IValueEntry? _baseValueEntry;
/// <summary>
/// Gets the current effective value as a boxed value.
/// </summary>
public object? Value => GetBoxedValue();
/// <summary>
/// Gets the priority of the current effective value.
/// </summary>
public BindingPriority Priority { get; protected set; }
/// <summary>
/// Gets the priority of the current base value.
/// </summary>
public BindingPriority BasePriority { get; protected set; }
/// <summary>
/// Begins a reevaluation pass on the effective value.
/// </summary>
/// <param name="clearLocalValue">
/// Determines whether any current local value should be cleared.
/// </param>
/// <remarks>
/// This method resets the <see cref="Priority"/> and <see cref="BasePriority"/> properties
/// to Unset, pending reevaluation.
/// </remarks>
public void BeginReevaluation(bool clearLocalValue = false)
{
if (clearLocalValue || Priority != BindingPriority.LocalValue)
Priority = BindingPriority.Unset;
if (clearLocalValue || BasePriority != BindingPriority.LocalValue)
BasePriority = BindingPriority.Unset;
}
/// <summary>
/// Ends a reevaluation pass on the effective value.
/// </summary>
/// <remarks>
/// This method unsubscribes from any unused value entries.
/// </remarks>
public void EndReevaluation()
{
if (Priority == BindingPriority.Unset)
{
_valueEntry?.Unsubscribe();
_valueEntry = null;
}
if (BasePriority == BindingPriority.Unset)
{
_baseValueEntry?.Unsubscribe();
_baseValueEntry = null;
}
}
/// <summary>
/// Sets the value and base value for a non-LocalValue priority, raising
/// <see cref="AvaloniaObject.PropertyChanged"/> where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="value">The new value of the property.</param>
/// <param name="priority">The priority of the new value.</param>
public abstract void SetAndRaise(
ValueStore owner,
IValueEntry value,
BindingPriority priority);
/// <summary>
/// Raises <see cref="AvaloniaObject.PropertyChanged"/> in response to an inherited value
/// change.
/// </summary>
/// <param name="owner">The owner object.</param>
/// <param name="property">The property being changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
public abstract void RaiseInheritedValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
EffectiveValue? oldValue,
EffectiveValue? newValue);
/// <summary>
/// Removes the current animation value and reverts to the base value, raising
/// <see cref="AvaloniaObject.PropertyChanged"/> where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property being changed.</param>
public abstract void RemoveAnimationAndRaise(ValueStore owner, AvaloniaProperty property);
/// <summary>
/// Coerces the property value.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property to coerce.</param>
public abstract void CoerceValue(ValueStore owner, AvaloniaProperty property);
/// <summary>
/// Disposes the effective value, raising <see cref="AvaloniaObject.PropertyChanged"/>
/// where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property being cleared.</param>
public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property);
protected abstract object? GetBoxedValue();
protected void UpdateValueEntry(IValueEntry? entry, BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
if (priority <= BindingPriority.Animation)
{
// If we've received an animation value and the current value is a non-animation
// value, then the current entry becomes our base entry.
if (Priority > BindingPriority.LocalValue && Priority < BindingPriority.Inherited)
{
Debug.Assert(_valueEntry is not null);
_baseValueEntry = _valueEntry;
_valueEntry = null;
}
if (_valueEntry != entry)
{
_valueEntry?.Unsubscribe();
_valueEntry = entry;
}
}
else if (Priority <= BindingPriority.Animation)
{
// We've received a non-animation value and have an active animation value, so the
// new entry becomes our base entry.
if (_baseValueEntry != entry)
{
_baseValueEntry?.Unsubscribe();
_baseValueEntry = entry;
}
}
else if (_valueEntry != entry)
{
// Both the current value and the new value are non-animation values, so the new
// entry replaces the existing entry.
_valueEntry?.Unsubscribe();
_valueEntry = entry;
}
}
protected void UnsubscribeValueEntries()
{
_valueEntry?.Unsubscribe();
_baseValueEntry?.Unsubscribe();
}
}
}

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

@ -0,0 +1,270 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents the active value for a property in a <see cref="ValueStore"/>.
/// </summary>
/// <remarks>
/// Stores the active value in an <see cref="AvaloniaObject"/>'s <see cref="ValueStore"/>
/// for a single property, when the value is not inherited or unset/default.
/// </remarks>
internal sealed class EffectiveValue<T> : EffectiveValue
{
private readonly StyledPropertyMetadata<T> _metadata;
private T? _baseValue;
private UncommonFields? _uncommon;
public EffectiveValue(AvaloniaObject owner, StyledPropertyBase<T> property)
{
Priority = BindingPriority.Unset;
BasePriority = BindingPriority.Unset;
_metadata = property.GetMetadata(owner.GetType());
var value = _metadata.DefaultValue;
if (property.HasCoercion && _metadata.CoerceValue is { } coerce)
{
_uncommon = new()
{
_coerce = coerce,
_uncoercedValue = value,
_uncoercedBaseValue = value,
};
Value = coerce(owner, value);
}
else
{
Value = value;
}
}
/// <summary>
/// Gets the current effective value.
/// </summary>
public new T Value { get; private set; }
public override void SetAndRaise(
ValueStore owner,
IValueEntry value,
BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
UpdateValueEntry(value, priority);
SetAndRaiseCore(owner, (StyledPropertyBase<T>)value.Property, GetValue(value), priority);
}
public void SetLocalValueAndRaise(
ValueStore owner,
StyledPropertyBase<T> property,
T value)
{
SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue);
}
public bool TryGetBaseValue([MaybeNullWhen(false)] out T value)
{
value = _baseValue!;
return BasePriority != BindingPriority.Unset;
}
public override void RaiseInheritedValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
EffectiveValue? oldValue,
EffectiveValue? newValue)
{
Debug.Assert(oldValue is not null || newValue is not null);
var p = (StyledPropertyBase<T>)property;
var o = oldValue is not null ? ((EffectiveValue<T>)oldValue).Value : _metadata.DefaultValue;
var n = newValue is not null ? ((EffectiveValue<T>)newValue).Value : _metadata.DefaultValue;
var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset;
if (!EqualityComparer<T>.Default.Equals(o, n))
{
owner.RaisePropertyChanged(p, o, n, priority, true);
}
}
public override void RemoveAnimationAndRaise(ValueStore owner, AvaloniaProperty property)
{
Debug.Assert(Priority != BindingPriority.Animation);
Debug.Assert(BasePriority != BindingPriority.Unset);
UpdateValueEntry(null, BindingPriority.Animation);
SetAndRaiseCore(owner, (StyledPropertyBase<T>)property, _baseValue!, BasePriority);
}
public override void CoerceValue(ValueStore owner, AvaloniaProperty property)
{
if (_uncommon is null)
return;
SetAndRaiseCore(
owner,
(StyledPropertyBase<T>)property,
_uncommon._uncoercedValue!,
Priority,
_uncommon._uncoercedBaseValue!,
BasePriority);
}
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
{
UnsubscribeValueEntries();
DisposeAndRaiseUnset(owner, (StyledPropertyBase<T>)property);
}
public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase<T> property)
{
BindingPriority priority;
T oldValue;
if (property.Inherits && owner.TryGetInheritedValue(property, out var i))
{
oldValue = ((EffectiveValue<T>)i).Value;
priority = BindingPriority.Inherited;
}
else
{
oldValue = _metadata.DefaultValue;
priority = BindingPriority.Unset;
}
if (!EqualityComparer<T>.Default.Equals(oldValue, Value))
{
owner.Owner.RaisePropertyChanged(property, Value, oldValue, priority, true);
if (property.Inherits)
owner.OnInheritedEffectiveValueDisposed(property, Value);
}
}
protected override object? GetBoxedValue() => Value;
private static T GetValue(IValueEntry entry)
{
if (entry is IValueEntry<T> typed)
return typed.GetValue();
else
return (T)entry.GetValue()!;
}
private void SetAndRaiseCore(
ValueStore owner,
StyledPropertyBase<T> property,
T value,
BindingPriority priority)
{
Debug.Assert(priority < BindingPriority.Inherited);
var oldValue = Value;
var valueChanged = false;
var baseValueChanged = false;
var v = value;
if (_uncommon?._coerce is { } coerce)
v = coerce(owner.Owner, value);
if (priority <= Priority)
{
valueChanged = !EqualityComparer<T>.Default.Equals(Value, v);
Value = v;
Priority = priority;
if (_uncommon is not null)
_uncommon._uncoercedValue = value;
}
if (priority <= BasePriority && priority >= BindingPriority.LocalValue)
{
baseValueChanged = !EqualityComparer<T>.Default.Equals(_baseValue, v);
_baseValue = v;
BasePriority = priority;
if (_uncommon is not null)
_uncommon._uncoercedBaseValue = value;
}
if (valueChanged)
{
using var notifying = PropertyNotifying.Start(owner.Owner, property);
owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true);
if (property.Inherits)
owner.OnInheritedEffectiveValueChanged(property, oldValue, this);
}
else if (baseValueChanged)
{
owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false);
}
}
private void SetAndRaiseCore(
ValueStore owner,
StyledPropertyBase<T> property,
T value,
BindingPriority priority,
T baseValue,
BindingPriority basePriority)
{
Debug.Assert(priority < BindingPriority.Inherited);
Debug.Assert(basePriority > BindingPriority.Animation);
Debug.Assert(priority <= basePriority);
var oldValue = Value;
var valueChanged = false;
var baseValueChanged = false;
var v = value;
var bv = baseValue;
if (_uncommon?._coerce is { } coerce)
{
v = coerce(owner.Owner, value);
bv = coerce(owner.Owner, baseValue);
}
if (priority != BindingPriority.Unset && !EqualityComparer<T>.Default.Equals(Value, v))
{
Value = v;
valueChanged = true;
if (_uncommon is not null)
_uncommon._uncoercedValue = value;
}
if (priority != BindingPriority.Unset &&
(BasePriority == BindingPriority.Unset ||
!EqualityComparer<T>.Default.Equals(_baseValue, bv)))
{
_baseValue = v;
baseValueChanged = true;
if (_uncommon is not null)
_uncommon._uncoercedValue = baseValue;
}
Priority = priority;
BasePriority = basePriority;
if (valueChanged)
{
using var notifying = PropertyNotifying.Start(owner.Owner, property);
owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true);
if (property.Inherits)
owner.OnInheritedEffectiveValueChanged(property, oldValue, this);
}
if (baseValueChanged)
{
owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false);
}
}
private class UncommonFields
{
public Func<IAvaloniaObject, T, T>? _coerce;
public T? _uncoercedValue;
public T? _uncoercedBaseValue;
}
}
}

8
src/Avalonia.Base/PropertyStore/IBatchUpdate.cs

@ -1,8 +0,0 @@
namespace Avalonia.PropertyStore
{
internal interface IBatchUpdate
{
void BeginBatchUpdate();
void EndBatchUpdate();
}
}

18
src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs

@ -1,18 +0,0 @@
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="IPriorityValueEntry{T}"/>.
/// </summary>
internal interface IPriorityValueEntry : IValue
{
}
/// <summary>
/// Represents an object that can act as an entry in a <see cref="PriorityValue{T}"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
internal interface IPriorityValueEntry<T> : IPriorityValueEntry, IValue<T>
{
void Reparent(PriorityValue<T> parent);
}
}

28
src/Avalonia.Base/PropertyStore/IValue.cs

@ -1,28 +0,0 @@
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="IValue{T}"/>.
/// </summary>
internal interface IValue
{
BindingPriority Priority { get; }
Optional<object?> GetValue();
void Start();
void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue);
}
/// <summary>
/// Represents an object that can act as an entry in a <see cref="ValueStore"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
internal interface IValue<T> : IValue
{
Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation);
}
}

30
src/Avalonia.Base/PropertyStore/IValueEntry.cs

@ -0,0 +1,30 @@
using System;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped value entry in a <see cref="ValueFrame"/>.
/// </summary>
internal interface IValueEntry
{
bool HasValue { get; }
/// <summary>
/// Gets the property that this value applies to.
/// </summary>
AvaloniaProperty Property { get; }
/// <summary>
/// Gets the value associated with the entry.
/// </summary>
/// <exception cref="AvaloniaInternalException">
/// The entry has no value.
/// </exception>
object? GetValue();
/// <summary>
/// Called when the value entry is removed from the value store.
/// </summary>
void Unsubscribe();
}
}

16
src/Avalonia.Base/PropertyStore/IValueEntry`1.cs

@ -0,0 +1,16 @@
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents a typed value entry in a <see cref="ValueFrame"/>.
/// </summary>
internal interface IValueEntry<T> : IValueEntry
{
/// <summary>
/// Gets the value associated with the entry.
/// </summary>
/// <exception cref="AvaloniaInternalException">
/// The entry has no value.
/// </exception>
new T GetValue();
}
}

31
src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs

@ -0,0 +1,31 @@
using System;
namespace Avalonia.PropertyStore
{
internal class ImmediateValueEntry<T> : IValueEntry<T>, IDisposable
{
private readonly ImmediateValueFrame _owner;
private readonly T _value;
public ImmediateValueEntry(
ImmediateValueFrame owner,
StyledPropertyBase<T> property,
T value)
{
_owner = owner;
_value = value;
Property = property;
}
public StyledPropertyBase<T> Property { get; }
public bool HasValue => true;
AvaloniaProperty IValueEntry.Property => Property;
public void Unsubscribe() { }
public void Dispose() => _owner.OnEntryDisposed(this);
object? IValueEntry.GetValue() => _value;
T IValueEntry<T>.GetValue() => _value;
}
}

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

@ -0,0 +1,63 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Holds values in a <see cref="ValueStore"/> set by one of the SetValue or AddBinding
/// overloads with non-LocalValue priority.
/// </summary>
internal class ImmediateValueFrame : ValueFrame
{
public ImmediateValueFrame(BindingPriority priority)
{
Priority = priority;
}
public TypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
var e = new TypedBindingEntry<T>(this, property, source);
Add(e);
return e;
}
public TypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<T> source)
{
var e = new TypedBindingEntry<T>(this, property, source);
Add(e);
return e;
}
public SourceUntypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<object?> source)
{
var e = new SourceUntypedBindingEntry<T>(this, property, source);
Add(e);
return e;
}
public ImmediateValueEntry<T> AddValue<T>(StyledPropertyBase<T> property, T value)
{
var e = new ImmediateValueEntry<T>(this, property, value);
Add(e);
return e;
}
public void OnEntryDisposed(IValueEntry value)
{
Remove(value.Property);
Owner?.OnValueEntryRemoved(this, value.Property);
}
protected override bool GetIsActive(out bool hasChanged)
{
hasChanged = false;
return true;
}
}
}

59
src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs

@ -0,0 +1,59 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal class LocalValueBindingObserver<T> : IObserver<T>,
IObserver<BindingValue<T>>,
IDisposable
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
public LocalValueBindingObserver(ValueStore owner, StyledPropertyBase<T> property)
{
_owner = owner;
Property = property;
}
public StyledPropertyBase<T> Property { get;}
public void Start(IObservable<T> source)
{
_subscription = source.Subscribe(this);
}
public void Start(IObservable<BindingValue<T>> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(T value)
{
if (Property.ValidateValue?.Invoke(value) != false)
_owner.SetValue(Property, value, BindingPriority.LocalValue);
else
_owner.ClearLocalValue(Property);
}
public void OnNext(BindingValue<T> value)
{
LoggingUtils.LogIfNecessary(_owner.Owner, Property, value);
if (value.HasValue)
_owner.SetValue(Property, value.Value, BindingPriority.LocalValue);
else if (value.Type != BindingValueType.DataValidationError)
_owner.ClearLocalValue(Property);
}
}
}

41
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -1,41 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Stores a value with local value priority in a <see cref="ValueStore"/> or
/// <see cref="PriorityValue{T}"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
internal class LocalValueEntry<T> : IValue<T>
{
private T _value;
public LocalValueEntry(T value) => _value = value;
public BindingPriority Priority => BindingPriority.LocalValue;
Optional<object?> IValue.GetValue() => new Optional<object?>(_value);
public Optional<T> GetValue(BindingPriority maxPriority)
{
return BindingPriority.LocalValue >= maxPriority ? _value : Optional<T>.Empty;
}
public void SetValue(T value) => _value = value;
public void Start() { }
public void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
{
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),
newValue.Cast<T>(),
BindingPriority.LocalValue));
}
}
}

62
src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs

@ -0,0 +1,62 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal class LocalValueUntypedBindingObserver<T> : IObserver<object?>,
IDisposable
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
public LocalValueUntypedBindingObserver(ValueStore owner, StyledPropertyBase<T> property)
{
_owner = owner;
Property = property;
}
public StyledPropertyBase<T> Property { get; }
public void Start(IObservable<object?> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(object? value)
{
if (value is BindingNotification n)
{
value = n.Value;
LoggingUtils.LogIfNecessary(_owner.Owner, Property, n);
}
if (value == AvaloniaProperty.UnsetValue)
{
_owner.ClearLocalValue(Property);
}
else if (value == BindingOperations.DoNothing)
{
// Do nothing!
}
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue))
{
_owner.SetValue(Property, typedValue, BindingPriority.LocalValue);
}
else
{
_owner.ClearLocalValue(Property);
LoggingUtils.LogInvalidValue(_owner.Owner, Property, typeof(T), value);
}
}
}
}

82
src/Avalonia.Base/PropertyStore/LoggingUtils.cs

@ -0,0 +1,82 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal static class LoggingUtils
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogIfNecessary(
AvaloniaObject owner,
AvaloniaProperty property,
BindingNotification value)
{
if (value.ErrorType != BindingErrorType.None)
Log(owner, property, value.Error!);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogIfNecessary<T>(
AvaloniaObject owner,
AvaloniaProperty property,
BindingValue<T> value)
{
if (value.HasError)
Log(owner, property, value.Error!);
}
public static void LogInvalidValue(
AvaloniaObject owner,
AvaloniaProperty property,
Type expectedType,
object? value)
{
if (value is not null)
{
owner.GetBindingWarningLogger(property, null)?.Log(
owner,
"Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})",
owner,
property,
expectedType,
value,
value.GetType());
}
else
{
owner.GetBindingWarningLogger(property, null)?.Log(
owner,
"Error in binding to {Target}.{Property}: expected {ExpectedType}, got null",
owner,
property,
expectedType);
}
}
private static void Log(
AvaloniaObject owner,
AvaloniaProperty property,
Exception e)
{
if (e is TargetInvocationException t)
e = t.InnerException!;
if (e is AggregateException a)
{
foreach (var i in a.InnerExceptions)
Log(owner, property, i);
}
else
{
owner.GetBindingWarningLogger(property, e)?.Log(
owner,
"Error in binding to {Target}.{Property}: {Message}",
owner,
property,
e.Message);
}
}
}
}

326
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -1,326 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="PriorityValue{T}"/>.
/// </summary>
interface IPriorityValue : IValue
{
void UpdateEffectiveValue();
}
/// <summary>
/// Stores a set of prioritized values and bindings in a <see cref="ValueStore"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <remarks>
/// When more than a single value or binding is applied to a property in an
/// <see cref="AvaloniaObject"/>, the entry in the <see cref="ValueStore"/> is converted into
/// a <see cref="PriorityValue{T}"/>. This class holds any number of
/// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
/// they were added) plus a local value.
/// </remarks>
internal class PriorityValue<T> : IPriorityValue, IValue<T>, IBatchUpdate
{
private readonly AvaloniaObject _owner;
private readonly ValueStore _store;
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _localValue;
private Optional<T> _value;
private bool _isCalculatingValue;
private bool _batchUpdate;
public PriorityValue(
AvaloniaObject owner,
StyledPropertyBase<T> property,
ValueStore store)
{
_owner = owner;
Property = property;
_store = store;
if (property.HasCoercion)
{
var metadata = property.GetMetadata(owner.GetType());
_coerceValue = metadata.CoerceValue;
}
}
public PriorityValue(
AvaloniaObject owner,
StyledPropertyBase<T> property,
ValueStore store,
IPriorityValueEntry<T> existing)
: this(owner, property, store)
{
existing.Reparent(this);
_entries.Add(existing);
if (existing is IBindingEntry binding &&
existing.Priority == BindingPriority.LocalValue)
{
// Bit of a special case here: if we have a local value binding that is being
// promoted to a priority value we need to make sure the binding is subscribed
// even if we've got a batch operation in progress because otherwise we don't know
// whether the binding or a subsequent SetValue with local priority will win. A
// notification won't be sent during batch update anyway because it will be
// caught and stored for later by the ValueStore.
binding.Start(ignoreBatchUpdate: true);
}
var v = existing.GetValue();
if (v.HasValue)
{
_value = v;
Priority = existing.Priority;
}
}
public PriorityValue(
AvaloniaObject owner,
StyledPropertyBase<T> property,
ValueStore sink,
LocalValueEntry<T> existing)
: this(owner, property, sink)
{
_value = _localValue = existing.GetValue(BindingPriority.LocalValue);
Priority = BindingPriority.LocalValue;
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; private set; } = BindingPriority.Unset;
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
Optional<object?> IValue.GetValue() => _value.ToObject();
public void BeginBatchUpdate()
{
_batchUpdate = true;
foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.BeginBatchUpdate();
}
}
public void EndBatchUpdate()
{
_batchUpdate = false;
foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.EndBatchUpdate();
}
UpdateEffectiveValue(null);
}
public void ClearLocalValue()
{
_localValue = default;
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
default,
default,
BindingPriority.LocalValue));
}
public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
{
if (Priority == BindingPriority.Unset)
{
return default;
}
if (Priority >= maxPriority)
{
return _value;
}
return CalculateValue(maxPriority).Item1;
}
public IDisposable? SetValue(T value, BindingPriority priority)
{
IDisposable? result = null;
if (priority == BindingPriority.LocalValue)
{
_localValue = value;
}
else
{
var insert = FindInsertPoint(priority);
var entry = new ConstantValueEntry<T>(Property, value, priority, new ValueOwner<T>(this));
_entries.Insert(insert, entry);
result = entry;
}
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
default,
value,
priority));
return result;
}
public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)
{
var binding = new BindingEntry<T>(_owner, Property, source, priority, new(this));
var insert = FindInsertPoint(binding.Priority);
_entries.Insert(insert, binding);
if (_batchUpdate)
{
binding.BeginBatchUpdate();
if (priority == BindingPriority.LocalValue)
{
binding.Start(ignoreBatchUpdate: true);
}
}
return binding;
}
public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
public void Start() => UpdateEffectiveValue(null);
public void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
{
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}
public void ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
{
if (change.Priority == BindingPriority.LocalValue)
{
_localValue = default;
}
if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
{
UpdateEffectiveValue(c);
}
}
public void Completed(IPriorityValueEntry entry, Optional<T> oldValue)
{
_entries.Remove((IPriorityValueEntry<T>)entry);
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
oldValue,
default,
entry.Priority));
}
private int FindInsertPoint(BindingPriority priority)
{
var result = _entries.Count;
for (var i = 0; i < _entries.Count; ++i)
{
if (_entries[i].Priority < priority)
{
result = i;
break;
}
}
return result;
}
public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
{
_isCalculatingValue = true;
try
{
for (var i = _entries.Count - 1; i >= 0; --i)
{
var entry = _entries[i];
if (entry.Priority < maxPriority)
{
continue;
}
entry.Start();
if (entry.Priority >= BindingPriority.LocalValue &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
var entryValue = entry.GetValue();
if (entryValue.HasValue)
{
return (entryValue, entry.Priority);
}
}
if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
return (default, BindingPriority.Unset);
}
finally
{
_isCalculatingValue = false;
}
}
private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)
{
var (value, priority) = CalculateValue(BindingPriority.Animation);
if (value.HasValue && _coerceValue != null)
{
value = _coerceValue(_owner, value.Value);
}
Priority = priority;
if (value != _value)
{
var old = _value;
_value = value;
_store.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,
old,
value,
Priority));
}
else if (change is object)
{
change.MarkNonEffectiveValue();
change.SetOldValue(default);
_store.ValueChanged(change);
}
}
}
}

35
src/Avalonia.Base/PropertyStore/PropertyNotifying.cs

@ -0,0 +1,35 @@
using System;
using System.Diagnostics;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Raises <see cref="AvaloniaProperty.Notifying"/> where necessary.
/// </summary>
/// <remarks>
/// Uses the disposable pattern to ensure that the closing Notifying call is made even in the
/// presence of exceptions.
/// </remarks>
internal readonly struct PropertyNotifying : IDisposable
{
private readonly AvaloniaObject _owner;
private readonly AvaloniaProperty _property;
private PropertyNotifying(AvaloniaObject owner, AvaloniaProperty property)
{
Debug.Assert(property.Notifying is not null);
_owner = owner;
_property = property;
_property.Notifying!(owner, true);
}
public void Dispose() => _property.Notifying!(_owner, false);
public static PropertyNotifying? Start(AvaloniaObject owner, AvaloniaProperty property)
{
if (property.Notifying is null)
return null;
return new PropertyNotifying(owner, property);
}
}
}

35
src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs

@ -0,0 +1,35 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// An <see cref="IValueEntry"/> that holds a binding whose source observable is untyped and
/// target property is typed.
/// </summary>
internal sealed class SourceUntypedBindingEntry<TTarget> : BindingEntryBase<TTarget, object?>
{
private readonly Func<TTarget, bool>? _validate;
public SourceUntypedBindingEntry(
ValueFrame frame,
StyledPropertyBase<TTarget> property,
IObservable<object?> source)
: base(frame, property, source)
{
_validate = property.ValidateValue;
}
public new StyledPropertyBase<TTarget> Property => (StyledPropertyBase<TTarget>)base.Property;
protected override BindingValue<TTarget> ConvertAndValidate(object? value)
{
return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate);
}
protected override BindingValue<TTarget> ConvertAndValidate(BindingValue<object?> value)
{
throw new NotSupportedException();
}
}
}

52
src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs

@ -0,0 +1,52 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// An <see cref="IValueEntry"/> that holds a binding whose source observable and target
/// property are both typed.
/// </summary>
internal sealed class TypedBindingEntry<T> : BindingEntryBase<T, T>
{
public TypedBindingEntry(
ValueFrame frame,
StyledPropertyBase<T> property,
IObservable<T> source)
: base(frame, property, source)
{
}
public TypedBindingEntry(
ValueFrame frame,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source)
: base(frame, property, source)
{
}
public new StyledPropertyBase<T> Property => (StyledPropertyBase<T>)base.Property;
protected override BindingValue<T> ConvertAndValidate(T value)
{
if (Property.ValidateValue?.Invoke(value) == false)
{
return BindingValue<T>.BindingError(
new InvalidCastException($"'{value}' is not a valid value."));
}
return value;
}
protected override BindingValue<T> ConvertAndValidate(BindingValue<T> value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
{
return BindingValue<T>.BindingError(
new InvalidCastException($"'{value.Value}' is not a valid value."));
}
return value;
}
}
}

33
src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// An <see cref="IValueEntry"/> that holds a binding whose source observable and target
/// property are both untyped.
/// </summary>
internal class UntypedBindingEntry : BindingEntryBase<object?, object?>
{
private readonly Func<object?, bool>? _validate;
public UntypedBindingEntry(
ValueFrame frame,
AvaloniaProperty property,
IObservable<object?> source)
: base(frame, property, source)
{
_validate = ((IStyledPropertyAccessor)property).ValidateValue;
}
protected override BindingValue<object?> ConvertAndValidate(object? value)
{
return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate);
}
protected override BindingValue<object?> ConvertAndValidate(BindingValue<object?> value)
{
throw new NotSupportedException();
}
}
}

55
src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs

@ -0,0 +1,55 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore
{
internal static class UntypedValueUtils
{
public static BindingValue<T> ConvertAndValidate<T>(
object? value,
Type targetType,
Func<T, bool>? validate)
{
var v = BindingValue<T>.FromUntyped(value, targetType);
if (v.HasValue && validate?.Invoke(v.Value) == false)
{
return BindingValue<T>.BindingError(
new InvalidCastException($"'{v.Value}' is not a valid value."));
}
return v;
}
public static bool TryConvertAndValidate(
AvaloniaProperty property,
object? value,
out object? result)
{
if (TypeUtilities.TryConvertImplicit(property.PropertyType, value, out result))
return ((IStyledPropertyAccessor)property).ValidateValue(result);
result = default;
return false;
}
public static bool TryConvertAndValidate<T>(
StyledPropertyBase<T> property,
object? value,
[MaybeNullWhen(false)] out T result)
{
if (TypeUtilities.TryConvertImplicit(typeof(T), value, out var v))
{
result = (T)v!;
if (property.ValidateValue?.Invoke(result) != false)
return true;
}
result = default;
return false;
}
}
}

103
src/Avalonia.Base/PropertyStore/ValueFrame.cs

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Utilities;
using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
namespace Avalonia.PropertyStore
{
internal abstract class ValueFrame
{
private List<IValueEntry>? _entries;
private AvaloniaPropertyDictionary<IValueEntry> _index;
private ValueStore? _owner;
private bool _isShared;
public int EntryCount => _index.Count;
public bool IsActive => GetIsActive(out _);
public ValueStore? Owner => !_isShared ? _owner :
throw new AvaloniaInternalException("Cannot get owner for shared ValueFrame");
public BindingPriority Priority { get; protected set; }
public bool Contains(AvaloniaProperty property) => _index.ContainsKey(property);
public IValueEntry GetEntry(int index) => _entries?[index] ?? _index[0];
public void SetOwner(ValueStore? owner)
{
if (_owner is not null && owner is not null)
throw new AvaloniaInternalException("ValueFrame already has an owner.");
if (!_isShared)
_owner = owner;
}
public bool TryGetEntryIfActive(
AvaloniaProperty property,
[NotNullWhen(true)] out IValueEntry? entry,
out bool activeChanged)
{
if (_index.TryGetValue(property, out entry))
return GetIsActive(out activeChanged);
activeChanged = false;
return false;
}
public void OnBindingCompleted(IValueEntry binding)
{
var property = binding.Property;
Remove(property);
Owner?.OnValueEntryRemoved(this, property);
}
public virtual void Dispose()
{
for (var i = 0; i < _index.Count; ++i)
_index[i].Unsubscribe();
}
protected abstract bool GetIsActive(out bool hasChanged);
protected void MakeShared()
{
_isShared = true;
_owner = null;
}
protected void Add(IValueEntry value)
{
Debug.Assert(!value.Property.IsDirect);
if (_entries is null && _index.Count == 1)
{
_entries = new();
_entries.Add(_index[0]);
}
_index.Add(value.Property, value);
_entries?.Add(value);
}
protected void Remove(AvaloniaProperty property)
{
Debug.Assert(!property.IsDirect);
if (_entries is not null)
{
var count = _entries.Count;
for (var i = 0; i < count; ++i)
{
if (_entries[i].Property == property)
{
_entries.RemoveAt(i);
break;
}
}
}
_index.Remove(property);
}
}
}

45
src/Avalonia.Base/PropertyStore/ValueOwner.cs

@ -1,45 +0,0 @@
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents a union type of <see cref="ValueStore"/> and <see cref="PriorityValue{T}"/>,
/// which are the valid owners of a value store <see cref="IValue"/>.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
internal readonly struct ValueOwner<T>
{
private readonly ValueStore? _store;
private readonly PriorityValue<T>? _priorityValue;
public ValueOwner(ValueStore o)
{
_store = o;
_priorityValue = null;
}
public ValueOwner(PriorityValue<T> v)
{
_store = null;
_priorityValue = v;
}
public bool IsValueStore => _store is not null;
public void Completed(StyledPropertyBase<T> property, IPriorityValueEntry entry, Optional<T> oldValue)
{
if (_store is not null)
_store?.Completed(property, entry, oldValue);
else
_priorityValue!.Completed(entry, oldValue);
}
public void ValueChanged(AvaloniaPropertyChangedEventArgs<T> e)
{
if (_store is not null)
_store?.ValueChanged(e);
else
_priorityValue!.ValueChanged(e);
}
}
}

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

@ -0,0 +1,962 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore
{
internal class ValueStore
{
private readonly List<ValueFrame> _frames = new();
private Dictionary<int, IDisposable>? _localValueBindings;
private AvaloniaPropertyDictionary<EffectiveValue> _effectiveValues;
private int _inheritedValueCount;
private int _isEvaluating;
private int _frameGeneration;
private int _styling;
public ValueStore(AvaloniaObject owner) => Owner = owner;
public AvaloniaObject Owner { get; }
public ValueStore? InheritanceAncestor { get; private set; }
public bool IsEvaluating => _isEvaluating > 0;
public IReadOnlyList<ValueFrame> Frames => _frames;
public void BeginStyling() => ++_styling;
public void EndStyling()
{
if (--_styling == 0)
ReevaluateEffectiveValues();
}
public void AddFrame(ValueFrame style)
{
InsertFrame(style);
ReevaluateEffectiveValues();
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<T> source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<object?> source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueUntypedBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<BindingValue<T>> source)
{
var observer = new DirectBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<T> source)
{
var observer = new DirectBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<object?> source)
{
var observer = new DirectUntypedBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
public void ClearLocalValue(AvaloniaProperty property)
{
if (TryGetEffectiveValue(property, out var effective) &&
effective.Priority == BindingPriority.LocalValue)
{
ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true);
}
}
public IDisposable? SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
{
if (property.ValidateValue?.Invoke(value) == false)
{
throw new ArgumentException($"{value} is not a valid value for '{property.Name}.");
}
if (priority != BindingPriority.LocalValue)
{
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddValue(property, value);
if (TryGetEffectiveValue(property, out var existing))
{
var effective = (EffectiveValue<T>)existing;
effective.SetAndRaise(this, result, priority);
}
else
{
var effectiveValue = new EffectiveValue<T>(Owner, property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetAndRaise(this, result, priority);
}
return result;
}
else
{
if (TryGetEffectiveValue(property, out var existing))
{
var effective = (EffectiveValue<T>)existing;
effective.SetLocalValueAndRaise(this, property, value);
}
else
{
var effectiveValue = new EffectiveValue<T>(Owner, property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetLocalValueAndRaise(this, property, value);
}
return null;
}
}
public object? GetValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return v.Value;
if (property.Inherits && TryGetInheritedValue(property, out v))
return v.Value;
return GetDefaultValue(property);
}
public T GetValue<T>(StyledPropertyBase<T> property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return ((EffectiveValue<T>)v).Value;
if (property.Inherits && TryGetInheritedValue(property, out v))
return ((EffectiveValue<T>)v).Value;
return property.GetDefaultValue(Owner.GetType());
}
public bool IsAnimating(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return v.Priority <= BindingPriority.Animation;
return false;
}
public bool IsSet(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return v.Priority < BindingPriority.Inherited;
return false;
}
public void CoerceValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
v.CoerceValue(this, property);
}
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property)
{
if (TryGetEffectiveValue(property, out var v) &&
((EffectiveValue<T>)v).TryGetBaseValue(out var baseValue))
{
return baseValue;
}
return default;
}
public bool TryGetInheritedValue(
AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? result)
{
Debug.Assert(property.Inherits);
var i = InheritanceAncestor;
while (i is not null)
{
if (i.TryGetEffectiveValue(property, out result))
return true;
i = i.InheritanceAncestor;
}
result = null;
return false;
}
public void SetInheritanceParent(AvaloniaObject? newParent)
{
var values = AvaloniaPropertyDictionaryPool<OldNewValue>.Get();
var oldAncestor = InheritanceAncestor;
var newAncestor = newParent?.GetValueStore();
if (newAncestor?._inheritedValueCount == 0)
newAncestor = newAncestor.InheritanceAncestor;
// The old and new inheritance ancestors are the same, nothing to do here.
if (oldAncestor == newAncestor)
return;
// First get the old values from the old inheritance ancestor.
var f = oldAncestor;
while (f is not null)
{
var count = f._effectiveValues.Count;
for (var i = 0; i < count; ++i)
{
f._effectiveValues.GetKeyValue(i, out var key, out var value);
if (key.Inherits)
values.TryAdd(key, new(value));
}
f = f.InheritanceAncestor;
}
f = newAncestor;
// Get the new values from the new inheritance ancestor.
while (f is not null)
{
var count = f._effectiveValues.Count;
for (var i = 0; i < count; ++i)
{
f._effectiveValues.GetKeyValue(i, out var key, out var value);
if (!key.Inherits)
continue;
if (values.TryGetValue(key, out var existing))
{
if (existing.NewValue is null)
values[key] = existing.WithNewValue(value);
}
else
{
values.Add(key, new(null, value));
}
}
f = f.InheritanceAncestor;
}
OnInheritanceAncestorChanged(newAncestor);
// Raise PropertyChanged events where necessary on this object and inheritance children.
{
var count = values.Count;
for (var i = 0; i < count; ++i)
{
values.GetKeyValue(i, out var key, out var v);
var oldValue = v.OldValue;
var newValue = v.NewValue;
if (oldValue != newValue)
InheritedValueChanged(key, oldValue, newValue);
}
}
AvaloniaPropertyDictionaryPool<OldNewValue>.Release(values);
}
/// <summary>
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
/// binding produces a new value.
/// </summary>
/// <param name="entry">The binding entry.</param>
/// <param name="priority">The priority of binding which produced a new value.</param>
public void OnBindingValueChanged(
IValueEntry entry,
BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
var property = entry.Property;
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.BasePriority)
ReevaluateEffectiveValue(property, existing);
}
else
{
AddEffectiveValueAndRaise(property, entry, priority);
}
}
/// <summary>
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
/// binding produces an unset value.
/// </summary>
/// <param name="property">The bound property.</param>
/// <param name="priority">The priority of binding which produced a new value.</param>
public void OnBindingValueCleared(AvaloniaProperty property, BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
}
/// <summary>
/// Called by a <see cref="ValueFrame"/> when its <see cref="ValueFrame.IsActive"/>
/// state changes.
/// </summary>
/// <param name="frame">The frame which produced the change.</param>
public void OnFrameActivationChanged(ValueFrame frame)
{
if (frame.EntryCount == 0)
return;
else if (frame.EntryCount == 1)
{
var property = frame.GetEntry(0).Property;
_effectiveValues.TryGetValue(property, out var current);
ReevaluateEffectiveValue(property, current);
}
else
ReevaluateEffectiveValues();
}
/// <summary>
/// Called by the parent value store when its inheritance ancestor changes.
/// </summary>
/// <param name="ancestor">The new inheritance ancestor.</param>
public void OnInheritanceAncestorChanged(ValueStore? ancestor)
{
if (ancestor != this)
{
InheritanceAncestor = ancestor;
if (_inheritedValueCount > 0)
return;
}
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnInheritanceAncestorChanged(ancestor);
}
}
/// <summary>
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
/// changes its value on this value store.
/// </summary>
/// <param name="property">The property whose value changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="value">The effective value instance.</param>
public void OnInheritedEffectiveValueChanged<T>(
StyledPropertyBase<T> property,
T oldValue,
EffectiveValue<T> value)
{
Debug.Assert(property.Inherits);
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, value.Value);
}
}
/// <summary>
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
/// is removed from the effective values.
/// </summary>
/// <param name="property">The property whose value changed.</param>
/// <param name="oldValue">The old value of the property.</param>
public void OnInheritedEffectiveValueDisposed<T>(StyledPropertyBase<T> property, T oldValue)
{
Debug.Assert(property.Inherits);
var children = Owner.GetInheritanceChildren();
if (children is not null)
{
var defaultValue = property.GetDefaultValue(Owner.GetType());
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, defaultValue);
}
}
}
/// <summary>
/// Called when a <see cref="LocalValueBindingObserver{T}"/> or
/// <see cref="DirectBindingObserver{T}"/> completes.
/// </summary>
/// <param name="property">The previously bound property.</param>
/// <param name="observer">The observer.</param>
public void OnLocalValueBindingCompleted(AvaloniaProperty property, IDisposable observer)
{
if (_localValueBindings is not null &&
_localValueBindings.TryGetValue(property.Id, out var existing))
{
if (existing == observer)
{
_localValueBindings?.Remove(property.Id);
ClearLocalValue(property);
}
}
}
/// <summary>
/// Called when an inherited property changes on the value store of the inheritance ancestor.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="property">The property.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
public void OnAncestorInheritedValueChanged<T>(
StyledPropertyBase<T> property,
T oldValue,
T newValue)
{
Debug.Assert(property.Inherits);
// If the inherited value is set locally, propagation stops here.
if (_effectiveValues.ContainsKey(property))
return;
using var notifying = PropertyNotifying.Start(Owner, property);
Owner.RaisePropertyChanged(
property,
oldValue,
newValue,
BindingPriority.Inherited,
true);
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue);
}
}
/// <summary>
/// Called by a <see cref="ValueFrame"/> to re-evaluate the effective value when a value
/// is removed.
/// </summary>
/// <param name="frame">The frame on which the change occurred.</param>
/// <param name="property">The property whose value was removed.</param>
public void OnValueEntryRemoved(ValueFrame frame, AvaloniaProperty property)
{
if (frame.EntryCount == 0)
_frames.Remove(frame);
if (TryGetEffectiveValue(property, out var existing))
{
if (frame.Priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
}
public bool RemoveFrame(ValueFrame frame)
{
if (_frames.Remove(frame))
{
frame.Dispose();
++_frameGeneration;
ReevaluateEffectiveValues();
}
return false;
}
public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property)
{
object? value;
BindingPriority priority;
if (_effectiveValues.TryGetValue(property, out var v))
{
value = v.Value;
priority = v.Priority;
}
else if (property.Inherits && TryGetInheritedValue(property, out v))
{
value = v.Value;
priority = BindingPriority.Inherited;
}
else
{
value = GetDefaultValue(property);
priority = BindingPriority.Unset;
}
return new AvaloniaPropertyValue(
property,
value,
priority,
null);
}
private int InsertFrame(ValueFrame frame)
{
// Uncomment this line when #8549 is fixed.
//Debug.Assert(!_frames.Contains(frame));
var index = BinarySearchFrame(frame.Priority);
_frames.Insert(index, frame);
++_frameGeneration;
frame.SetOwner(this);
return index;
}
private ImmediateValueFrame GetOrCreateImmediateValueFrame(
AvaloniaProperty property,
BindingPriority priority,
out int frameIndex)
{
Debug.Assert(priority != BindingPriority.LocalValue);
var index = BinarySearchFrame(priority);
if (index > 0 && _frames[index - 1] is ImmediateValueFrame f &&
f.Priority == priority &&
!f.Contains(property))
{
frameIndex = index - 1;
return f;
}
var result = new ImmediateValueFrame(priority);
frameIndex = InsertFrame(result);
return result;
}
private void AddEffectiveValue(AvaloniaProperty property, EffectiveValue effectiveValue)
{
_effectiveValues.Add(property, effectiveValue);
if (property.Inherits && _inheritedValueCount++ == 0)
OnInheritanceAncestorChanged(this);
}
/// <summary>
/// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
/// event and notifies inheritance children if necessary .
/// </summary>
/// <param name="property">The property.</param>
/// <param name="entry">The value entry.</param>
/// <param name="priority">The value priority.</param>
private void AddEffectiveValueAndRaise(AvaloniaProperty property, IValueEntry entry, BindingPriority priority)
{
Debug.Assert(priority < BindingPriority.Inherited);
var effectiveValue = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetAndRaise(this, entry, priority);
}
private void RemoveEffectiveValue(AvaloniaProperty property, int index)
{
_effectiveValues.RemoveAt(index);
if (property.Inherits && --_inheritedValueCount == 0)
OnInheritanceAncestorChanged(InheritanceAncestor);
}
private bool RemoveEffectiveValue(AvaloniaProperty property)
{
if (_effectiveValues.Remove(property))
{
if (property.Inherits && --_inheritedValueCount == 0)
OnInheritanceAncestorChanged(InheritanceAncestor);
return true;
}
return false;
}
private void InheritedValueChanged(
AvaloniaProperty property,
EffectiveValue? oldValue,
EffectiveValue? newValue)
{
Debug.Assert(oldValue != newValue);
Debug.Assert(oldValue is not null || newValue is not null);
// If the value is set locally, propagaton ends here.
if (_effectiveValues.ContainsKey(property) == true)
return;
using var notifying = PropertyNotifying.Start(Owner, property);
// Raise PropertyChanged on this object if necessary.
(oldValue ?? newValue!).RaiseInheritedValueChanged(Owner, property, oldValue, newValue);
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().InheritedValueChanged(property, oldValue, newValue);
}
}
private void ReevaluateEffectiveValue(
AvaloniaProperty property,
EffectiveValue? current,
bool ignoreLocalValue = false)
{
++_isEvaluating;
try
{
restart:
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
// it has finished.
if (_styling > 0)
return;
var generation = _frameGeneration;
// Notify the existing effective value that reevaluation is starting.
current?.BeginReevaluation(ignoreLocalValue);
// Iterate the frames to get the effective value.
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
var priority = frame.Priority;
var foundEntry = frame.TryGetEntryIfActive(property, out var entry, out var activeChanged);
// If the active state of the frame has changed since the last read, and
// the frame holds multiple values then we need to re-evaluate the
// effective values of all properties.
if (activeChanged && frame.EntryCount > 1)
{
ReevaluateEffectiveValues();
return;
}
// We're interested in the value if:
// - There is no current effective value, or
// - The value's priority is higher than the current effective value's priority, or
// - The value is a non-animation value and its priority is higher than the current
// effective value's base priority
var isRelevantPriority = current is null ||
priority < current.Priority ||
(priority > BindingPriority.Animation && priority < current.BasePriority);
if (foundEntry && isRelevantPriority && entry!.HasValue)
{
if (current is not null)
{
current.SetAndRaise(this, entry, priority);
}
else
{
current = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, current);
current.SetAndRaise(this, entry, priority);
}
}
if (generation != _frameGeneration)
goto restart;
if (current?.Priority < BindingPriority.Unset &&
current?.BasePriority < BindingPriority.Unset)
break;
}
current?.EndReevaluation();
if (current?.Priority == BindingPriority.Unset)
{
if (current.BasePriority == BindingPriority.Unset)
{
RemoveEffectiveValue(property);
current.DisposeAndRaiseUnset(this, property);
}
else
{
current.RemoveAnimationAndRaise(this, property);
}
}
}
finally
{
--_isEvaluating;
}
}
private void ReevaluateEffectiveValues()
{
++_isEvaluating;
try
{
restart:
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
// it has finished.
if (_styling > 0)
return;
var generation = _frameGeneration;
var count = _effectiveValues.Count;
// Notify the existing effective values that reevaluation is starting.
for (var i = 0; i < count; ++i)
_effectiveValues[i].BeginReevaluation();
// Iterate the frames, setting and creating effective values.
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
if (!frame.IsActive)
continue;
var priority = frame.Priority;
count = frame.EntryCount;
for (var j = 0; j < count; ++j)
{
var entry = frame.GetEntry(j);
var property = entry.Property;
// Skip if we already have a value/base value for this property.
if (_effectiveValues.TryGetValue(property, out var effectiveValue) &&
effectiveValue.BasePriority < BindingPriority.Unset)
continue;
if (!entry.HasValue)
continue;
if (effectiveValue is not null)
{
effectiveValue.SetAndRaise(this, entry, priority);
}
else
{
var v = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, v);
v.SetAndRaise(this, entry, priority);
}
if (generation != _frameGeneration)
goto restart;
}
}
// Remove all effective values that are still unset.
for (var i = _effectiveValues.Count - 1; i >= 0; --i)
{
_effectiveValues.GetKeyValue(i, out var key, out var e);
e.EndReevaluation();
if (e.Priority == BindingPriority.Unset)
{
RemoveEffectiveValue(key, i);
e.DisposeAndRaiseUnset(this, key);
if (i > _effectiveValues.Count)
break;
}
}
}
finally
{
--_isEvaluating;
}
}
private bool TryGetEffectiveValue(
AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? value)
{
if (_effectiveValues.TryGetValue(property, out value))
return true;
value = null;
return false;
}
private EffectiveValue? GetEffectiveValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var value))
return value;
return null;
}
private object? GetDefaultValue(AvaloniaProperty property)
{
return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner.GetType());
}
private void DisposeExistingLocalValueBinding(AvaloniaProperty property)
{
if (_localValueBindings is not null &&
_localValueBindings.TryGetValue(property.Id, out var existing))
{
existing.Dispose();
}
}
private int BinarySearchFrame(BindingPriority priority)
{
var lo = 0;
var hi = _frames.Count - 1;
// Binary search insertion point.
while (lo <= hi)
{
var i = lo + ((hi - lo) >> 1);
var order = priority - _frames[i].Priority;
if (order <= 0)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
}
return lo;
}
private readonly struct OldNewValue
{
public OldNewValue(EffectiveValue? oldValue)
{
OldValue = oldValue;
NewValue = null;
}
public OldNewValue(EffectiveValue? oldValue, EffectiveValue? newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
public readonly EffectiveValue? OldValue;
public readonly EffectiveValue? NewValue;
public OldNewValue WithNewValue(EffectiveValue newValue) => new(OldValue, newValue);
}
}
}

59
src/Avalonia.Base/Reactive/BindingValueAdapter.cs

@ -1,59 +0,0 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Reactive
{
internal class BindingValueAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
IObserver<T>
{
private readonly IObservable<T> _source;
private IDisposable? _subscription;
public BindingValueAdapter(IObservable<T> source) => _source = source;
public void OnCompleted() => PublishCompleted();
public void OnError(Exception error) => PublishError(error);
public void OnNext(T value) => PublishNext(BindingValue<T>.FromUntyped(value));
protected override void Subscribed() => _subscription = _source.Subscribe(this);
protected override void Unsubscribed() => _subscription?.Dispose();
}
internal class BindingValueSubjectAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
ISubject<BindingValue<T>>
{
private readonly ISubject<T> _source;
private readonly Inner _inner;
private IDisposable? _subscription;
public BindingValueSubjectAdapter(ISubject<T> source)
{
_source = source;
_inner = new Inner(this);
}
public void OnCompleted() => _source.OnCompleted();
public void OnError(Exception error) => _source.OnError(error);
public void OnNext(BindingValue<T> value)
{
if (value.HasValue)
{
_source.OnNext(value.Value);
}
}
protected override void Subscribed() => _subscription = _source.Subscribe(_inner);
protected override void Unsubscribed() => _subscription?.Dispose();
private class Inner : IObserver<T>
{
private readonly BindingValueSubjectAdapter<T> _owner;
public Inner(BindingValueSubjectAdapter<T> owner) => _owner = owner;
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(T value) => _owner.PublishNext(BindingValue<T>.FromUntyped(value));
}
}
}

33
src/Avalonia.Base/Reactive/BindingValueExtensions.cs

@ -1,33 +0,0 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Reactive
{
public static class BindingValueExtensions
{
public static IObservable<BindingValue<T>> ToBindingValue<T>(this IObservable<T> source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
return new BindingValueAdapter<T>(source);
}
public static ISubject<BindingValue<T>> ToBindingValue<T>(this ISubject<T> source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
return new BindingValueSubjectAdapter<T>(source);
}
public static IObservable<object?> ToUntyped<T>(this IObservable<BindingValue<T>> source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
return new UntypedBindingAdapter<T>(source);
}
public static ISubject<object?> ToUntyped<T>(this ISubject<BindingValue<T>> source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
return new UntypedBindingSubjectAdapter<T>(source);
}
}
}

6
src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs

@ -51,10 +51,11 @@ namespace Avalonia.Reactive
protected void PublishCompleted()
{
_completed = true;
if (_observer != null)
{
_observer.OnCompleted();
_completed = true;
Unsubscribed();
_observer = null;
}
@ -62,10 +63,11 @@ namespace Avalonia.Reactive
protected void PublishError(Exception error)
{
_error = error;
if (_observer != null)
{
_observer.OnError(error);
_error = error;
Unsubscribed();
_observer = null;
}

62
src/Avalonia.Base/Reactive/TypedBindingAdapter.cs

@ -1,62 +0,0 @@
using System;
using Avalonia.Data;
using Avalonia.Logging;
namespace Avalonia.Reactive
{
internal class TypedBindingAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
IObserver<BindingValue<object?>>
{
private readonly IAvaloniaObject _target;
private readonly AvaloniaProperty<T> _property;
private readonly IObservable<BindingValue<object?>> _source;
private IDisposable? _subscription;
public TypedBindingAdapter(
IAvaloniaObject target,
AvaloniaProperty<T> property,
IObservable<BindingValue<object?>> source)
{
_target = target;
_property = property;
_source = source;
}
public void OnNext(BindingValue<object?> value)
{
try
{
PublishNext(value.Convert<T>());
}
catch (InvalidCastException e)
{
var unwrappedValue = value.HasValue ? value.Value : null;
Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log(
_target,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
_property.Name,
_property.PropertyType,
unwrappedValue,
unwrappedValue?.GetType());
PublishNext(BindingValue<T>.BindingError(e));
}
}
public void OnCompleted() => PublishCompleted();
public void OnError(Exception error) => PublishError(error);
public static IObservable<BindingValue<T>> Create(
IAvaloniaObject target,
AvaloniaProperty<T> property,
IObservable<BindingValue<object?>> source)
{
return source is IObservable<BindingValue<T>> result ?
result :
new TypedBindingAdapter<T>(target, property, source);
}
protected override void Subscribed() => _subscription = _source.Subscribe(this);
protected override void Unsubscribed() => _subscription?.Dispose();
}
}

55
src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs

@ -1,55 +0,0 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Reactive
{
internal class UntypedBindingAdapter<T> : SingleSubscriberObservableBase<object?>,
IObserver<BindingValue<T>>
{
private readonly IObservable<BindingValue<T>> _source;
private IDisposable? _subscription;
public UntypedBindingAdapter(IObservable<BindingValue<T>> source) => _source = source;
public void OnCompleted() => PublishCompleted();
public void OnError(Exception error) => PublishError(error);
public void OnNext(BindingValue<T> value) => value.ToUntyped();
protected override void Subscribed() => _subscription = _source.Subscribe(this);
protected override void Unsubscribed() => _subscription?.Dispose();
}
internal class UntypedBindingSubjectAdapter<T> : SingleSubscriberObservableBase<object?>,
ISubject<object?>
{
private readonly ISubject<BindingValue<T>> _source;
private readonly Inner _inner;
private IDisposable? _subscription;
public UntypedBindingSubjectAdapter(ISubject<BindingValue<T>> source)
{
_source = source;
_inner = new Inner(this);
}
public void OnCompleted() => _source.OnCompleted();
public void OnError(Exception error) => _source.OnError(error);
public void OnNext(object? value)
{
_source.OnNext(BindingValue<T>.FromUntyped(value));
}
protected override void Subscribed() => _subscription = _source.Subscribe(_inner);
protected override void Unsubscribed() => _subscription?.Dispose();
private class Inner : IObserver<BindingValue<T>>
{
private readonly UntypedBindingSubjectAdapter<T> _owner;
public Inner(UntypedBindingSubjectAdapter<T> owner) => _owner = owner;
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(BindingValue<T> value) => _owner.PublishNext(value.ToUntyped());
}
}
}

149
src/Avalonia.Base/StyledElement.cs

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Animation;
using Avalonia.Collections;
using Avalonia.Controls;
@ -69,7 +70,6 @@ namespace Avalonia
private IResourceDictionary? _resources;
private Styles? _styles;
private bool _styled;
private List<IStyleInstance>? _appliedStyles;
private ITemplatedControl? _templatedParent;
private bool _dataContextUpdating;
private bool _hasPromotedTheme;
@ -333,7 +333,7 @@ namespace Avalonia
throw new InvalidOperationException("BeginInit was not called.");
}
if (--_initCount == 0 && _logicalRoot != null)
if (--_initCount == 0 && _logicalRoot is not null)
{
ApplyStyling();
InitializeIfNeeded();
@ -351,18 +351,25 @@ namespace Avalonia
{
if (_initCount == 0 && !_styled)
{
try
{
BeginBatchUpdate();
AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
}
finally
var styler = AvaloniaLocator.Current.GetService<IStyler>();
var hasPromotedTheme = _hasPromotedTheme;
if (styler is object)
{
_styled = true;
EndBatchUpdate();
GetValueStore().BeginStyling();
try
{
styler.ApplyStyles(this);
}
finally
{
_styled = true;
GetValueStore().EndStyling();
}
}
if (_hasPromotedTheme)
if (hasPromotedTheme)
{
_hasPromotedTheme = false;
ClearValue(ThemeProperty);
@ -389,14 +396,15 @@ namespace Avalonia
internal StyleDiagnostics GetStyleDiagnosticsInternal()
{
IReadOnlyList<IStyleInstance>? appliedStyles = _appliedStyles;
var styles = new List<IStyleInstance>();
if (appliedStyles is null)
foreach (var frame in GetValueStore().Frames)
{
appliedStyles = Array.Empty<IStyleInstance>();
if (frame is IStyleInstance style)
styles.Add(style);
}
return new StyleDiagnostics(appliedStyles);
return new StyleDiagnostics(styles);
}
/// <inheritdoc/>
@ -522,20 +530,8 @@ namespace Avalonia
return null;
}
void IStyleable.StyleApplied(IStyleInstance instance)
{
instance = instance ?? throw new ArgumentNullException(nameof(instance));
_appliedStyles ??= new List<IStyleInstance>();
_appliedStyles.Add(instance);
}
void IStyleable.DetachStyles() => DetachStyles();
void IStyleable.DetachStyles(IReadOnlyList<IStyle> styles) => DetachStyles(styles);
void IStyleable.InvalidateStyles() => InvalidateStyles();
void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
{
InvalidateStylesOnThisAndDescendents();
@ -643,13 +639,15 @@ namespace Avalonia
if (change.Property == ThemeProperty)
{
var (oldValue, newValue) = change.GetOldAndNewValue<ControlTheme?>();
// Changing the theme detaches all styles, meaning that if the theme property was
// set via a style, it will get cleared! To work around this, if the value was
// applied at less than local value priority then promote the value to local value
// priority until styling is re-applied.
if (change.Priority > BindingPriority.LocalValue)
{
Theme = change.GetNewValue<ControlTheme?>();
Theme = newValue;
_hasPromotedTheme = true;
}
else if (_hasPromotedTheme && change.Priority == BindingPriority.LocalValue)
@ -658,9 +656,16 @@ namespace Avalonia
}
InvalidateStyles();
if (oldValue is not null)
DetachControlThemeFromTemplateChildren(oldValue);
}
}
internal virtual void DetachControlThemeFromTemplateChildren(ControlTheme theme)
{
}
private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted)
{
if (o is StyledElement element)
@ -830,59 +835,25 @@ namespace Avalonia
}
}
private void DetachStyles()
private void DetachStyles(IReadOnlyList<StyleBase>? styles = null)
{
if (_appliedStyles?.Count > 0)
{
BeginBatchUpdate();
var valueStore = GetValueStore();
try
{
foreach (var i in _appliedStyles)
{
i.Dispose();
}
valueStore.BeginStyling();
_appliedStyles.Clear();
}
finally
for (var i = valueStore.Frames.Count - 1; i >= 0; --i)
{
if (valueStore.Frames[i] is StyleInstance si &&
(styles is null || styles.Contains(si.Source)))
{
EndBatchUpdate();
valueStore.RemoveFrame(si);
}
}
valueStore.EndStyling();
_styled = false;
}
private void DetachStyles(IReadOnlyList<IStyle> styles)
{
styles = styles ?? throw new ArgumentNullException(nameof(styles));
if (_appliedStyles is null)
{
return;
}
var count = styles.Count;
for (var i = 0; i < count; ++i)
{
for (var j = _appliedStyles.Count - 1; j >= 0; --j)
{
var applied = _appliedStyles[j];
if (applied.Source == styles[i])
{
_appliedStyles.RemoveAt(j);
applied.Dispose();
}
if (j > _appliedStyles.Count)
j = _appliedStyles.Count;
}
}
}
private void InvalidateStylesOnThisAndDescendents()
{
InvalidateStyles();
@ -898,7 +869,7 @@ namespace Avalonia
}
}
private void DetachStylesFromThisAndDescendents(IReadOnlyList<IStyle> styles)
private void DetachStylesFromThisAndDescendents(IReadOnlyList<StyleBase> styles)
{
DetachStyles(styles);
@ -930,38 +901,24 @@ namespace Avalonia
}
}
private static IReadOnlyList<IStyle> RecurseStyles(IReadOnlyList<IStyle> styles)
private static IReadOnlyList<StyleBase> RecurseStyles(IReadOnlyList<IStyle> styles)
{
var count = styles.Count;
List<IStyle>? result = null;
for (var i = 0; i < count; ++i)
{
var style = styles[i];
if (style.Children.Count > 0)
{
if (result is null)
{
result = new List<IStyle>(styles);
}
RecurseStyles(style.Children, result);
}
}
return result ?? styles;
var result = new List<StyleBase>();
RecurseStyles(styles, result);
return result;
}
private static void RecurseStyles(IReadOnlyList<IStyle> styles, List<IStyle> result)
private static void RecurseStyles(IReadOnlyList<IStyle> styles, List<StyleBase> result)
{
var count = styles.Count;
for (var i = 0; i < count; ++i)
{
var style = styles[i];
result.Add(style);
RecurseStyles(style.Children, result);
var s = styles[i];
if (s is StyleBase style)
result.Add(style);
else if (s is IReadOnlyList<IStyle> children)
RecurseStyles(children, result);
}
}
}

86
src/Avalonia.Base/StyledPropertyBase.cs

@ -1,7 +1,10 @@
using System;
using System.Reflection;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Styling;
using Avalonia.Utilities;
namespace Avalonia
{
@ -169,6 +172,20 @@ namespace Avalonia
/// <inheritdoc/>
object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
bool IStyledPropertyAccessor.ValidateValue(object? value)
{
if (value is null && !typeof(TValue).IsValueType)
return ValidateValue?.Invoke(default!) ?? true;
if (value is TValue typed)
return ValidateValue?.Invoke(typed) ?? true;
return false;
}
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{
return new EffectiveValue<TValue>(o, this);
}
/// <inheritdoc/>
internal override void RouteClearValue(AvaloniaObject o)
{
@ -182,77 +199,44 @@ namespace Avalonia
}
/// <inheritdoc/>
internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority)
internal override object? RouteGetBaseValue(AvaloniaObject o)
{
var value = o.GetBaseValue<TValue>(this, maxPriority);
var value = o.GetBaseValue<TValue>(this);
return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/>
internal override IDisposable? RouteSetValue(
AvaloniaObject o,
AvaloniaObject target,
object? value,
BindingPriority priority)
{
var v = TryConvert(value);
if (v.HasValue)
if (value == BindingOperations.DoNothing)
{
return o.SetValue<TValue>(this, (TValue)v.Value!, priority);
return null;
}
else if (v.Type == BindingValueType.UnsetValue)
else if (value == UnsetValue)
{
o.ClearValue(this);
target.ClearValue(this);
return null;
}
else if (v.HasError)
else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted))
{
throw v.Error!;
return target.SetValue<TValue>(this, (TValue)converted!, priority);
}
else
{
var type = value?.GetType().FullName ?? "(null)";
throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})");
}
return null;
}
/// <inheritdoc/>
internal override IDisposable RouteBind(
AvaloniaObject o,
IObservable<BindingValue<object?>> source,
AvaloniaObject target,
IObservable<object?> source,
BindingPriority priority)
{
var adapter = TypedBindingAdapter<TValue>.Create(o, this, source);
return o.Bind<TValue>(this, adapter, priority);
}
/// <inheritdoc/>
internal override void RouteInheritanceParentChanged(
AvaloniaObject o,
AvaloniaObject? oldParent)
{
o.InheritanceParentChanged(this, oldParent);
}
internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value)
{
if (value is IBinding binding)
{
return new PropertySetterBindingInstance<TValue>(
target,
this,
binding);
}
else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType))
{
return new PropertySetterTemplateInstance<TValue>(
target,
this,
template);
}
else
{
return new PropertySetterInstance<TValue>(
target,
this,
(TValue)value!);
}
return target.Bind<TValue>(this, source, priority);
}
private object? GetDefaultBoxedValue(Type type)

42
src/Avalonia.Base/Styling/Activators/AndActivator.cs

@ -1,6 +1,4 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Generic;
namespace Avalonia.Styling.Activators
{
@ -11,47 +9,45 @@ namespace Avalonia.Styling.Activators
internal class AndActivator : StyleActivatorBase, IStyleActivatorSink
{
private List<IStyleActivator>? _sources;
private ulong _flags;
private ulong _mask;
public int Count => _sources?.Count ?? 0;
public void Add(IStyleActivator activator)
{
if (IsSubscribed)
throw new AvaloniaInternalException("AndActivator is already subscribed.");
_sources ??= new List<IStyleActivator>();
_sources.Add(activator);
}
void IStyleActivatorSink.OnNext(bool value, int tag)
void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive();
protected override bool EvaluateIsActive()
{
if (value)
{
_flags |= 1ul << tag;
}
else
{
_flags &= ~(1ul << tag);
}
if (_sources is null || _sources.Count == 0)
return true;
if (_mask != 0)
var count = _sources.Count;
var mask = (1ul << count) - 1;
var flags = 0UL;
for (var i = 0; i < count; ++i)
{
PublishNext(_flags == _mask);
if (_sources[i].GetIsActive())
flags |= 1ul << i;
}
return flags == mask;
}
protected override void Initialize()
{
if (_sources is object)
{
var i = 0;
foreach (var source in _sources)
{
source.Subscribe(this, i++);
source.Subscribe(this);
}
_mask = (1ul << Count) - 1;
PublishNext(_flags == _mask);
}
}
@ -64,8 +60,6 @@ namespace Avalonia.Styling.Activators
source.Unsubscribe(this);
}
}
_mask = 0;
}
}
}

28
src/Avalonia.Base/Styling/Activators/IStyleActivator.cs

@ -9,21 +9,39 @@ namespace Avalonia.Styling.Activators
/// <remarks>
/// A style activator is very similar to an `IObservable{bool}` but is optimized for the
/// particular use-case of activating a style according to a selector. It differs from
/// an observable in two major ways:
/// an observable in three major ways:
///
/// - Can only have a single subscription
/// - The subscription can have a tag associated with it, allowing a subscriber to index
/// into a list of subscriptions without having to allocate additional objects.
/// - The activation state can be re-evaluated at any time by calling <see cref="GetIsActive"/>
/// - No error or completion messages
/// </remarks>
[Unstable]
public interface IStyleActivator : IDisposable
{
/// <summary>
/// Gets a value indicating whether the style is subscribed.
/// </summary>
bool IsSubscribed { get; }
/// <summary>
/// Gets the current activation state.
/// </summary>
/// <remarks>
/// This method should read directly from its inputs and not rely on any subscriptions
/// to fire in order to be up-to-date. If a change in active state occurs when reading
/// this method then any subscribed <see cref="IStyleActivatorSink"/> should not be
/// notified of the change.
/// </remarks>
bool GetIsActive();
/// <summary>
/// Subscribes to the activator.
/// </summary>
/// <param name="sink">The listener.</param>
/// <param name="tag">An optional tag.</param>
void Subscribe(IStyleActivatorSink sink, int tag = 0);
/// <remarks>
/// This method should not call <see cref="IStyleActivatorSink.OnNext(bool, int)"/>.
/// </remarks>
void Subscribe(IStyleActivatorSink sink);
/// <summary>
/// Unsubscribes from the activator.

3
src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs

@ -12,7 +12,6 @@ namespace Avalonia.Styling.Activators
/// Called when the subscribed activator value changes.
/// </summary>
/// <param name="value">The new value.</param>
/// <param name="tag">The subscription tag.</param>
void OnNext(bool value, int tag);
void OnNext(bool value);
}
}

9
src/Avalonia.Base/Styling/Activators/NotActivator.cs

@ -1,6 +1,4 @@
#nullable enable
namespace Avalonia.Styling.Activators
namespace Avalonia.Styling.Activators
{
/// <summary>
/// An <see cref="IStyleActivator"/> which inverts the state of an input activator.
@ -9,8 +7,9 @@ namespace Avalonia.Styling.Activators
{
private readonly IStyleActivator _source;
public NotActivator(IStyleActivator source) => _source = source;
void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value);
protected override void Initialize() => _source.Subscribe(this, 0);
void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive();
protected override bool EvaluateIsActive() => !_source.GetIsActive();
protected override void Initialize() => _source.Subscribe(this);
protected override void Deinitialize() => _source.Unsubscribe(this);
}
}

19
src/Avalonia.Base/Styling/Activators/NthChildActivator.cs

@ -26,31 +26,26 @@ namespace Avalonia.Styling.Activators
_reversed = reversed;
}
protected override void Initialize()
protected override bool EvaluateIsActive()
{
PublishNext(IsMatching());
_provider.ChildIndexChanged += ChildIndexChanged;
return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
}
protected override void Deinitialize()
{
_provider.ChildIndexChanged -= ChildIndexChanged;
}
protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged;
protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged;
private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{
// Run matching again if:
// 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
// 2. e.Child is null, when all children indeces were changed.
// 2. e.Child is null, when all children indices were changed.
// 3. Subscribed child index was changed.
if (_reversed
|| e.Child is null
|| e.Child is null
|| e.Child == _control)
{
PublishNext(IsMatching());
ReevaluateIsActive();
}
}
private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
}
}

36
src/Avalonia.Base/Styling/Activators/OrActivator.cs

@ -1,6 +1,4 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Generic;
namespace Avalonia.Styling.Activators
{
@ -11,8 +9,6 @@ namespace Avalonia.Styling.Activators
internal class OrActivator : StyleActivatorBase, IStyleActivatorSink
{
private List<IStyleActivator>? _sources;
private ulong _flags;
private bool _initializing;
public int Count => _sources?.Count ?? 0;
@ -22,38 +18,30 @@ namespace Avalonia.Styling.Activators
_sources.Add(activator);
}
void IStyleActivatorSink.OnNext(bool value, int tag)
void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive();
protected override bool EvaluateIsActive()
{
if (value)
{
_flags |= 1ul << tag;
}
else
{
_flags &= ~(1ul << tag);
}
if (_sources is null || _sources.Count == 0)
return true;
if (!_initializing)
foreach (var source in _sources)
{
PublishNext(_flags != 0);
if (source.GetIsActive())
return true;
}
return false;
}
protected override void Initialize()
{
if (_sources is object)
{
var i = 0;
_initializing = true;
foreach (var source in _sources)
{
source.Subscribe(this, i++);
source.Subscribe(this);
}
_initializing = false;
PublishNext(_flags != 0);
}
}

10
src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs

@ -1,7 +1,5 @@
using System;
#nullable enable
namespace Avalonia.Styling.Activators
{
/// <summary>
@ -24,6 +22,12 @@ namespace Avalonia.Styling.Activators
_value = value;
}
protected override bool EvaluateIsActive()
{
var value = _control.GetValue(_property);
return PropertyEqualsSelector.Compare(_property.PropertyType, value, _value);
}
protected override void Initialize()
{
_subscription = _control.GetObservable(_property).Subscribe(this);
@ -33,6 +37,6 @@ namespace Avalonia.Styling.Activators
void IObserver<object?>.OnCompleted() { }
void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.OnNext(object? value) => PublishNext(PropertyEqualsSelector.Compare(_property.PropertyType, value, _value));
void IObserver<object?>.OnNext(object? value) => ReevaluateIsActive();
}
}

65
src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs

@ -1,5 +1,3 @@
#nullable enable
namespace Avalonia.Styling.Activators
{
/// <summary>
@ -8,26 +6,30 @@ namespace Avalonia.Styling.Activators
internal abstract class StyleActivatorBase : IStyleActivator
{
private IStyleActivatorSink? _sink;
private int _tag;
private bool? _value;
private bool _value;
public bool GetIsActive() => _value = EvaluateIsActive();
public bool IsSubscribed => _sink is not null;
public void Subscribe(IStyleActivatorSink sink, int tag = 0)
public void Subscribe(IStyleActivatorSink sink)
{
if (_sink is null)
{
_sink = sink;
_tag = tag;
_value = null;
Initialize();
_sink = sink;
}
else
{
throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once.");
throw new AvaloniaInternalException("StyleActivator is already subscribed.");
}
}
public void Unsubscribe(IStyleActivatorSink sink)
{
if (_sink is null)
return;
if (_sink != sink)
{
throw new AvaloniaInternalException("StyleActivatorSink is not subscribed.");
@ -37,22 +39,51 @@ namespace Avalonia.Styling.Activators
Deinitialize();
}
public void PublishNext(bool value)
public void Dispose()
{
if (_value != value)
_sink = null;
Deinitialize();
}
/// <summary>
/// Evaluates the activation state.
/// </summary>
/// <remarks>
/// This method should read directly from its inputs and not rely on any subscriptions to
/// fire in order to be up-to-date.
/// </remarks>
protected abstract bool EvaluateIsActive();
/// <summary>
/// Called from a derived class when the activation state should be re-evaluated and the
/// subscriber notified of any change.
/// </summary>
/// <returns>
/// The evaluated active state;
/// </returns>
protected bool ReevaluateIsActive()
{
var value = EvaluateIsActive();
if (value != _value)
{
_value = value;
_sink?.OnNext(value, _tag);
_sink?.OnNext(value);
}
}
public void Dispose()
{
_sink = null;
Deinitialize();
return value;
}
/// <summary>
/// Called in response to a <see cref="Subscribe(IStyleActivatorSink)"/> to allow the
/// derived class to set up any necessary subscriptions.
/// </summary>
protected abstract void Initialize();
/// <summary>
/// Called in response to an <see cref="Unsubscribe(IStyleActivatorSink)"/> or
/// <see cref="Dispose"/> to allow the derived class to dispose any active subscriptions.
/// </summary>
protected abstract void Deinitialize();
}
}

25
src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs

@ -1,10 +1,6 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls;
#nullable enable
namespace Avalonia.Styling.Activators
{
/// <summary>
@ -52,22 +48,9 @@ namespace Avalonia.Styling.Activators
return remainingMatches == 0;
}
void IClassesChangedListener.Changed()
{
PublishNext(IsMatching());
}
protected override void Initialize()
{
PublishNext(IsMatching());
_classes.AddListener(this);
}
protected override void Deinitialize()
{
_classes.RemoveListener(this);
}
private bool IsMatching() => AreClassesMatching(_classes, _match);
void IClassesChangedListener.Changed() => ReevaluateIsActive();
protected override bool EvaluateIsActive() => AreClassesMatching(_classes, _match);
protected override void Initialize() => _classes.AddListener(this);
protected override void Deinitialize() => _classes.RemoveListener(this);
}
}

4
src/Avalonia.Base/Styling/ChildSelector.cs

@ -27,11 +27,11 @@ namespace Avalonia.Styling
/// <inheritdoc/>
public override Type? TargetType => null;
public override string ToString()
public override string ToString(Style? owner)
{
if (_selectorString == null)
{
_selectorString = _parent.ToString() + " > ";
_selectorString = _parent.ToString(owner) + " > ";
}
return _selectorString;

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

@ -1,4 +1,5 @@
using System;
using Avalonia.PropertyStore;
namespace Avalonia.Styling
{
@ -51,13 +52,7 @@ namespace Avalonia.Styling
return result;
}
public override string ToString()
{
if (TargetType is not null)
return "ControlTheme: " + TargetType.Name;
else
return "ControlTheme";
}
public override string ToString() => TargetType?.Name ?? "ControlTheme";
internal override void SetParent(StyleBase? parent)
{

4
src/Avalonia.Base/Styling/DescendentSelector.cs

@ -25,11 +25,11 @@ namespace Avalonia.Styling
/// <inheritdoc/>
public override Type? TargetType => null;
public override string ToString()
public override string ToString(Style? owner)
{
if (_selectorString == null)
{
_selectorString = _parent.ToString() + ' ';
_selectorString = _parent.ToString(owner) + ' ';
}
return _selectorString;

6
src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs

@ -0,0 +1,6 @@
namespace Avalonia.Styling
{
internal class DirectPropertySetterBindingInstance : ISetterInstance
{
}
}

12
src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Styling
{
internal class DirectPropertySetterInstance : ISetterInstance
{
}
}

6
src/Avalonia.Base/Styling/ISetter.cs

@ -12,13 +12,13 @@ namespace Avalonia.Styling
/// <summary>
/// Instances a setter on a control.
/// </summary>
/// <param name="styleInstance">The style which contains the setter.</param>
/// <param name="target">The control.</param>
/// <returns>An <see cref="ISetterInstance"/>.</returns>
/// <remarks>
/// This method should return an <see cref="ISetterInstance"/> which can be used to apply
/// the setter to the specified control. Note that it should not apply the setter value
/// until <see cref="ISetterInstance.Start(bool)"/> is called.
/// the setter to the specified control.
/// </remarks>
ISetterInstance Instance(IStyleable target);
ISetterInstance Instance(IStyleInstance styleInstance, IStyleable target);
}
}

34
src/Avalonia.Base/Styling/ISetterInstance.cs

@ -1,40 +1,12 @@
using System;
using Avalonia.Metadata;
using Avalonia.Metadata;
namespace Avalonia.Styling
{
/// <summary>
/// Represents a setter that has been instanced on a control.
/// Represents an <see cref="ISetter"/> that has been instanced on a control.
/// </summary>
[Unstable]
public interface ISetterInstance : IDisposable
public interface ISetterInstance
{
/// <summary>
/// Starts the setter instance.
/// </summary>
/// <param name="hasActivator">Whether the parent style has an activator.</param>
/// <remarks>
/// If <paramref name="hasActivator"/> is false then the setter should be immediately
/// applied and <see cref="Activate"/> and <see cref="Deactivate"/> should not be called.
/// If true, then bindings etc should be initiated but not produce a value until
/// <see cref="Activate"/> called.
/// </remarks>
public void Start(bool hasActivator);
/// <summary>
/// Activates the setter.
/// </summary>
/// <remarks>
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
/// </remarks>
public void Activate();
/// <summary>
/// Deactivates the setter.
/// </summary>
/// <remarks>
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
/// </remarks>
public void Deactivate();
}
}

19
src/Avalonia.Base/Styling/IStyleInstance.cs

@ -1,13 +1,12 @@
using System;
using Avalonia.Metadata;
using Avalonia.Metadata;
namespace Avalonia.Styling
{
/// <summary>
/// Represents a style that has been instanced on a control.
/// Represents a <see cref="Style"/> that has been instanced on a control.
/// </summary>
[Unstable]
public interface IStyleInstance : IDisposable
public interface IStyleInstance
{
/// <summary>
/// Gets the source style.
@ -15,18 +14,16 @@ namespace Avalonia.Styling
IStyle Source { get; }
/// <summary>
/// Gets a value indicating whether this style has an activator.
/// Gets a value indicating whether this style instance has an activator.
/// </summary>
/// <remarks>
/// A style instance without an activator will always be active.
/// </remarks>
bool HasActivator { get; }
/// <summary>
/// Gets a value indicating whether this style is active.
/// </summary>
bool IsActive { get; }
/// <summary>
/// Instructs the style to start acting upon the control.
/// </summary>
void Start();
}
}

20
src/Avalonia.Base/Styling/IStyleable.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Metadata;
@ -31,25 +30,6 @@ namespace Avalonia.Styling
/// </summary>
ControlTheme? GetEffectiveTheme();
/// <summary>
/// Notifies the element that a style has been applied.
/// </summary>
/// <param name="instance">The style instance.</param>
void StyleApplied(IStyleInstance instance);
/// <summary>
/// Detaches all styles applied to the element.
/// </summary>
void DetachStyles();
/// <summary>
/// Detaches a collection of styles, if applied to the element.
/// </summary>
void DetachStyles(IReadOnlyList<IStyle> styles);
/// <summary>
/// Detaches all styles from the element and queues a restyle.
/// </summary>
void InvalidateStyles();
}
}

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

@ -11,7 +11,7 @@ namespace Avalonia.Styling
public override bool IsCombinator => false;
public override Type? TargetType => null;
public override string ToString() => "^";
public override string ToString(Style? owner) => owner?.Parent?.ToString() ?? "^";
protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{

4
src/Avalonia.Base/Styling/NotSelector.cs

@ -35,11 +35,11 @@ namespace Avalonia.Styling
public override Type? TargetType => _previous?.TargetType;
/// <inheritdoc/>
public override string ToString()
public override string ToString(Style? owner)
{
if (_selectorString == null)
{
_selectorString = $"{_previous?.ToString()}:not({_argument})";
_selectorString = $"{_previous?.ToString(owner)}:not({_argument})";
}
return _selectorString;

4
src/Avalonia.Base/Styling/NthChildSelector.cs

@ -107,11 +107,11 @@ namespace Avalonia.Styling
protected override Selector? MovePrevious() => _previous;
protected override Selector? MovePreviousOrParent() => _previous;
public override string ToString()
public override string ToString(Style? owner)
{
var expectedCapacity = NthLastChildSelectorName.Length + 8;
var stringBuilder = StringBuilderCache.Acquire(expectedCapacity);
stringBuilder.Append(_previous?.ToString());
stringBuilder.Append(_previous?.ToString(owner));
stringBuilder.Append(':');
stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName);

5
src/Avalonia.Base/Styling/OrSelector.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Styling.Activators;
#nullable enable
@ -55,11 +56,11 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public override string ToString()
public override string ToString(Style? owner)
{
if (_selectorString == null)
{
_selectorString = string.Join(", ", _selectors);
_selectorString = string.Join(", ", _selectors.Select(x => x.ToString(owner)));
}
return _selectorString;

4
src/Avalonia.Base/Styling/PropertyEqualsSelector.cs

@ -38,7 +38,7 @@ namespace Avalonia.Styling
public override Type? TargetType => _previous?.TargetType;
/// <inheritdoc/>
public override string ToString()
public override string ToString(Style? owner)
{
if (_selectorString == null)
{
@ -46,7 +46,7 @@ namespace Avalonia.Styling
if (_previous != null)
{
builder.Append(_previous.ToString());
builder.Append(_previous.ToString(owner));
}
builder.Append('[');

196
src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs

@ -1,200 +1,60 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Reactive;
#nullable enable
using Avalonia.PropertyStore;
namespace Avalonia.Styling
{
/// <summary>
/// A <see cref="Setter"/> which has been instanced on a control and has an
/// <see cref="IBinding"/> as its value.
/// </summary>
/// <typeparam name="T">The target property type.</typeparam>
internal class PropertySetterBindingInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
ISubject<BindingValue<T>>,
ISetterInstance
internal class PropertySetterBindingInstance : UntypedBindingEntry, ISetterInstance
{
private readonly IStyleable _target;
private readonly StyledPropertyBase<T>? _styledProperty;
private readonly DirectPropertyBase<T>? _directProperty;
private readonly InstancedBinding? _binding;
private readonly Inner _inner;
private BindingValue<T> _value;
private IDisposable? _subscription;
private IDisposable? _subscriptionTwoWay;
private IDisposable? _innerSubscription;
private bool _isActive;
private readonly AvaloniaObject _target;
private readonly BindingMode _mode;
public PropertySetterBindingInstance(
IStyleable target,
StyledPropertyBase<T> property,
IBinding binding)
AvaloniaObject target,
StyleInstance instance,
AvaloniaProperty property,
BindingMode mode,
IObservable<object?> source)
: base(instance, property, source)
{
_target = target;
_styledProperty = property;
_binding = binding.Initiate(_target, property);
_mode = mode;
if (_binding?.Mode == BindingMode.OneTime)
if (mode == BindingMode.TwoWay &&
source is not IObserver<object?>)
{
// For the moment, we don't support OneTime bindings in setters, because I'm not
// sure what the semantics should be in the case of activation/deactivation.
throw new NotSupportedException("OneTime bindings are not supported in setters.");
throw new NotSupportedException(
"Attempting to bind two-way with a binding source which doesn't support it.");
}
_inner = new Inner(this);
}
public PropertySetterBindingInstance(
IStyleable target,
DirectPropertyBase<T> property,
IBinding binding)
public override void Unsubscribe()
{
_target = target;
_directProperty = property;
_binding = binding.Initiate(_target, property);
_inner = new Inner(this);
_target.PropertyChanged -= PropertyChanged;
base.Unsubscribe();
}
public void Start(bool hasActivator)
protected override void Start(bool produceValue)
{
if (_binding is null)
return;
_isActive = !hasActivator;
if (_styledProperty is object)
if (!IsSubscribed)
{
if (_binding.Mode != BindingMode.OneWayToSource)
if (_mode == BindingMode.TwoWay)
{
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
_subscription = _target.Bind(_styledProperty, this, priority);
var observer = (IObserver<object?>)Source;
_target.PropertyChanged += PropertyChanged;
}
if (_binding.Mode == BindingMode.TwoWay)
{
_subscriptionTwoWay = _target.GetBindingObservable(_styledProperty).Subscribe(this);
}
}
else
{
if (_binding.Mode != BindingMode.OneWayToSource)
{
_subscription = _target.Bind(_directProperty!, this);
}
if (_binding.Mode == BindingMode.TwoWay)
{
_subscriptionTwoWay = _target.GetBindingObservable(_directProperty!).Subscribe(this);
}
base.Start(produceValue);
}
}
public void Activate()
private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (_binding is null)
return;
if (!_isActive)
if (e.Property == Property && e.Priority >= BindingPriority.LocalValue)
{
_innerSubscription ??= _binding.Observable!.Subscribe(_inner);
_isActive = true;
PublishNext();
if (Frame.Owner is not null && !Frame.Owner.IsEvaluating)
((IObserver<object?>)Source).OnNext(e.NewValue);
}
}
public void Deactivate()
{
if (_isActive)
{
_isActive = false;
_innerSubscription?.Dispose();
_innerSubscription = null;
PublishNext();
}
}
public override void Dispose()
{
if (_subscription is object)
{
var sub = _subscription;
_subscription = null;
sub.Dispose();
}
if (_subscriptionTwoWay is object)
{
var sub = _subscriptionTwoWay;
_subscriptionTwoWay = null;
sub.Dispose();
}
base.Dispose();
}
void IObserver<BindingValue<T>>.OnCompleted()
{
// This is the observable coming from the target control. It should not complete.
}
void IObserver<BindingValue<T>>.OnError(Exception error)
{
// This is the observable coming from the target control. It should not error.
}
void IObserver<BindingValue<T>>.OnNext(BindingValue<T> value)
{
if (value.HasValue && _isActive && _binding?.Subject is not null)
{
_binding.Subject.OnNext(value.Value);
}
}
protected override void Subscribed()
{
if (_isActive && _binding?.Observable is not null)
{
if (_innerSubscription is null)
{
_innerSubscription ??= _binding.Observable!.Subscribe(_inner);
}
else
{
PublishNext();
}
}
}
protected override void Unsubscribed()
{
_innerSubscription?.Dispose();
_innerSubscription = null;
}
private void PublishNext()
{
PublishNext(_isActive ? _value : default);
}
private void ConvertAndPublishNext(object? value)
{
_value = BindingValue<T>.FromUntyped(value);
if (_isActive)
{
PublishNext();
}
}
private class Inner : IObserver<object?>
{
private readonly PropertySetterBindingInstance<T> _owner;
public Inner(PropertySetterBindingInstance<T> owner) => _owner = owner;
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(object? value) => _owner.ConvertAndPublishNext(value);
}
}
}

121
src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs

@ -1,127 +1,24 @@
using System;
using Avalonia.Data;
using Avalonia.Reactive;
#nullable enable
using Avalonia.PropertyStore;
namespace Avalonia.Styling
{
/// <summary>
/// A <see cref="Setter"/> which has been instanced on a control and whose value is lazily
/// evaluated.
/// </summary>
/// <typeparam name="T">The target property type.</typeparam>
internal class PropertySetterTemplateInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
ISetterInstance
internal class PropertySetterTemplateInstance : IValueEntry, ISetterInstance
{
private readonly IStyleable _target;
private readonly StyledPropertyBase<T>? _styledProperty;
private readonly DirectPropertyBase<T>? _directProperty;
private readonly ITemplate _template;
private BindingValue<T> _value;
private IDisposable? _subscription;
private bool _isActive;
public PropertySetterTemplateInstance(
IStyleable target,
StyledPropertyBase<T> property,
ITemplate template)
{
_target = target;
_styledProperty = property;
_template = template;
}
private object? _value;
public PropertySetterTemplateInstance(
IStyleable target,
DirectPropertyBase<T> property,
ITemplate template)
public PropertySetterTemplateInstance(AvaloniaProperty property, ITemplate template)
{
_target = target;
_directProperty = property;
_template = template;
Property = property;
}
public void Start(bool hasActivator)
{
_isActive = !hasActivator;
if (_styledProperty is not null)
{
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
_subscription = _target.Bind(_styledProperty, this, priority);
}
else
{
_subscription = _target.Bind(_directProperty!, this);
}
}
public void Activate()
{
if (!_isActive)
{
_isActive = true;
PublishNext();
}
}
public void Deactivate()
{
if (_isActive)
{
_isActive = false;
PublishNext();
}
}
public override void Dispose()
{
if (_subscription is not null)
{
var sub = _subscription;
_subscription = null;
sub.Dispose();
}
else if (_isActive)
{
if (_styledProperty is not null)
{
_target.ClearValue(_styledProperty);
}
else
{
_target.ClearValue(_directProperty!);
}
}
base.Dispose();
}
public bool HasValue => true;
public AvaloniaProperty Property { get; }
protected override void Subscribed() => PublishNext();
protected override void Unsubscribed() { }
public object? GetValue() => _value ??= _template.Build();
private void EnsureTemplate()
{
if (_value.HasValue)
{
return;
}
_value = (T) _template.Build();
}
private void PublishNext()
{
if (_isActive)
{
EnsureTemplate();
PublishNext(_value);
}
else
{
PublishNext(default);
}
}
void IValueEntry.Unsubscribe() { }
}
}

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

@ -67,6 +67,15 @@ namespace Avalonia.Styling
return match;
}
public override string ToString() => ToString(null);
/// <summary>
/// Gets a string representing the selector, with the nesting separator (`^`) replaced with
/// the parent selector.
/// </summary>
/// <param name="owner">The owner style.</param>
public abstract string ToString(Style? owner);
/// <summary>
/// Evaluates the selector for a match.
/// </summary>

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

@ -2,8 +2,7 @@ using System;
using Avalonia.Animation;
using Avalonia.Data;
using Avalonia.Metadata;
#nullable enable
using Avalonia.PropertyStore;
namespace Avalonia.Styling
{
@ -14,9 +13,10 @@ namespace Avalonia.Styling
/// A <see cref="Setter"/> is used to set a <see cref="AvaloniaProperty"/> value on a
/// <see cref="AvaloniaObject"/> depending on a condition.
/// </remarks>
public class Setter : ISetter, IAnimationSetter
public class Setter : ISetter, IValueEntry, ISetterInstance, IAnimationSetter
{
private object? _value;
private DirectPropertySetterInstance? _direct;
/// <summary>
/// Initializes a new instance of the <see cref="Setter"/> class.
@ -30,7 +30,7 @@ namespace Avalonia.Styling
/// </summary>
/// <param name="property">The property to set.</param>
/// <param name="value">The property value.</param>
public Setter(AvaloniaProperty property, object value)
public Setter(AvaloniaProperty property, object? value)
{
Property = property;
Value = value;
@ -57,16 +57,72 @@ namespace Avalonia.Styling
}
}
public ISetterInstance Instance(IStyleable target)
{
target = target ?? throw new ArgumentNullException(nameof(target));
bool IValueEntry.HasValue => true;
AvaloniaProperty IValueEntry.Property => EnsureProperty();
public override string ToString() => $"Setter: {Property} = {Value}";
void IValueEntry.Unsubscribe() { }
ISetterInstance ISetter.Instance(IStyleInstance instance, IStyleable target)
{
if (target is not AvaloniaObject ao)
throw new InvalidOperationException("Don't know how to instance a style on this type.");
if (Property is null)
{
throw new InvalidOperationException("Setter.Property must be set.");
if (Property.IsDirect && instance.HasActivator)
throw new InvalidOperationException(
$"Cannot set direct property '{Property}' in '{instance.Source}' because the style has an activator.");
if (Value is IBinding binding)
return SetBinding((StyleInstance)instance, ao, binding);
else if (Value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(Property.PropertyType))
return new PropertySetterTemplateInstance(Property, template);
else if (!Property.IsValidValue(Value))
throw new InvalidCastException($"Setter value '{Value}' is not a valid value for property '{Property}'.");
else if (Property.IsDirect)
return SetDirectValue(target);
else
return this;
}
object? IValueEntry.GetValue() => Value;
private AvaloniaProperty EnsureProperty()
{
return Property ?? throw new InvalidOperationException("Setter.Property must be set.");
}
private ISetterInstance SetBinding(StyleInstance instance, AvaloniaObject target, IBinding binding)
{
if (!Property!.IsDirect)
{
var i = binding.Initiate(target, Property)!;
var mode = i.Mode;
if (mode == BindingMode.Default)
{
mode = Property!.GetMetadata(target.GetType()).DefaultBindingMode;
}
if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay)
{
return new PropertySetterBindingInstance(target, instance, Property, mode, i.Observable!);
}
throw new NotSupportedException();
}
else
{
target.Bind(Property, binding);
return new DirectPropertySetterBindingInstance();
}
}
return Property.CreateSetterInstance(target, Value);
private ISetterInstance SetDirectValue(IStyleable target)
{
target.SetValue(Property!, Value);
return _direct ??= new DirectPropertySetterInstance();
}
}
}

15
src/Avalonia.Base/Styling/Style.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.PropertyStore;
namespace Avalonia.Styling
{
@ -48,7 +49,9 @@ namespace Avalonia.Styling
SelectorMatch.NeverThisInstance);
if (match.IsMatch)
{
Attach(target, match.Activator);
}
result = match.Result;
}
@ -65,17 +68,7 @@ namespace Avalonia.Styling
/// Returns a string representation of the style.
/// </summary>
/// <returns>A string representation of the style.</returns>
public override string ToString()
{
if (Selector != null)
{
return "Style: " + Selector.ToString();
}
else
{
return "Style";
}
}
public override string ToString() => Selector?.ToString(this) ?? "Style";
internal override void SetParent(StyleBase? parent)
{

45
src/Avalonia.Base/Styling/StyleBase.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Metadata;
using Avalonia.PropertyStore;
using Avalonia.Styling.Activators;
namespace Avalonia.Styling
@ -18,6 +19,7 @@ namespace Avalonia.Styling
private List<ISetter>? _setters;
private List<IAnimation>? _animations;
private StyleCache? _childCache;
private StyleInstance? _sharedInstance;
public IList<IStyle> Children => _children ??= new(this);
@ -80,11 +82,46 @@ namespace Avalonia.Styling
return _resources?.TryGetResource(key, out result) ?? false;
}
internal void Attach(IStyleable target, IStyleActivator? activator)
internal ValueFrame Attach(IStyleable target, IStyleActivator? activator)
{
var instance = new StyleInstance(this, target, _setters, _animations, activator);
target.StyleApplied(instance);
instance.Start();
if (target is not AvaloniaObject ao)
throw new InvalidOperationException("Styles can only be applied to AvaloniaObjects.");
StyleInstance instance;
if (_sharedInstance is not null)
{
instance = _sharedInstance;
}
else
{
var canShareInstance = activator is null;
instance = new StyleInstance(this, activator);
if (_setters is not null)
{
foreach (var setter in _setters)
{
var setterInstance = setter.Instance(instance, target);
instance.Add(setterInstance);
canShareInstance &= setterInstance == setter;
}
}
if (_animations is not null)
instance.Add(_animations);
if (canShareInstance)
{
instance.MakeShared();
_sharedInstance = instance;
}
}
ao.GetValueStore().AddFrame(instance);
instance.ApplyAnimations(ao);
return instance;
}
internal SelectorMatchResult TryAttachChildren(IStyleable target, object? host)

162
src/Avalonia.Base/Styling/StyleInstance.cs

@ -1,137 +1,103 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Subjects;
using Avalonia.Animation;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// A <see cref="Style"/> which has been instanced on a control.
/// Stores state for a <see cref="Style"/> that has been instanced on a control.
/// </summary>
internal sealed class StyleInstance : IStyleInstance, IStyleActivatorSink
/// <remarks>
/// <see cref="StyleInstance"/> is based on <see cref="ValueFrame"/> meaning that it is
/// injected directly into the value store of an <see cref="AvaloniaObject"/>. Depending on
/// the setters present on the style, it may be possible to share a single style instance
/// among all controls that the style is applied to, meaning that a single style instance can
/// apply to multiple controls.
/// </remarks>
internal class StyleInstance : ValueFrame, IStyleInstance, IStyleActivatorSink, IDisposable
{
private readonly ISetterInstance[]? _setters;
private readonly IDisposable[]? _animations;
private readonly IStyleActivator? _activator;
private readonly Subject<bool>? _animationTrigger;
private bool _isActive;
private List<ISetterInstance>? _setters;
private List<IAnimation>? _animations;
private Subject<bool>? _animationTrigger;
public StyleInstance(
IStyle source,
IStyleable target,
IReadOnlyList<ISetter>? setters,
IReadOnlyList<IAnimation>? animations,
IStyleActivator? activator = null)
public StyleInstance(IStyle style, IStyleActivator? activator)
{
Source = source ?? throw new ArgumentNullException(nameof(source));
Target = target ?? throw new ArgumentNullException(nameof(target));
_activator = activator;
IsActive = _activator is null;
if (setters is not null)
{
var setterCount = setters.Count;
_setters = new ISetterInstance[setterCount];
for (var i = 0; i < setterCount; ++i)
{
_setters[i] = setters[i].Instance(Target);
}
}
if (animations is not null && target is Animatable animatable)
{
var animationsCount = animations.Count;
_animations = new IDisposable[animationsCount];
_animationTrigger = new Subject<bool>();
for (var i = 0; i < animationsCount; ++i)
{
_animations[i] = animations[i].Apply(animatable, null, _animationTrigger);
}
}
Priority = activator is object ? BindingPriority.StyleTrigger : BindingPriority.Style;
Source = style;
}
public bool HasActivator => _activator is not null;
public bool IsActive { get; private set; }
public bool HasActivator => _activator is object;
public IStyle Source { get; }
public IStyleable Target { get; }
public void Start()
bool IStyleInstance.IsActive => _isActive;
public void Add(ISetterInstance instance)
{
var hasActivator = HasActivator;
if (_setters is not null)
{
foreach (var setter in _setters)
{
setter.Start(hasActivator);
}
}
if (hasActivator)
if (instance is IValueEntry valueEntry)
{
_activator!.Subscribe(this, 0);
}
else if (_animationTrigger is not null)
{
_animationTrigger.OnNext(true);
if (Contains(valueEntry.Property))
throw new InvalidOperationException(
$"Duplicate setter encountered for property '{valueEntry.Property}' in '{Source}'.");
Add(valueEntry);
}
else
(_setters ??= new()).Add(instance);
}
public void Dispose()
public void Add(IList<IAnimation> animations)
{
if (_setters is not null)
{
foreach (var setter in _setters)
{
setter.Dispose();
}
}
if (_animations is null)
_animations = new List<IAnimation>(animations);
else
_animations.AddRange(animations);
}
if (_animations is not null)
public void ApplyAnimations(AvaloniaObject control)
{
if (_animations is not null && control is Animatable animatable)
{
foreach (var subscription in _animations)
{
subscription.Dispose();
}
_animationTrigger ??= new Subject<bool>();
foreach (var animation in _animations)
animation.Apply(animatable, null, _animationTrigger);
}
}
public override void Dispose()
{
base.Dispose();
_activator?.Dispose();
}
private void ActivatorChanged(bool value)
public new void MakeShared() => base.MakeShared();
void IStyleActivatorSink.OnNext(bool value)
{
if (IsActive != value)
{
IsActive = value;
Owner?.OnFrameActivationChanged(this);
_animationTrigger?.OnNext(value);
}
_animationTrigger?.OnNext(value);
protected override bool GetIsActive(out bool hasChanged)
{
var previous = _isActive;
if (_setters is not null)
{
if (IsActive)
{
foreach (var setter in _setters)
{
setter.Activate();
}
}
else
{
foreach (var setter in _setters)
{
setter.Deactivate();
}
}
}
if (_activator?.IsSubscribed == false)
{
_activator.Subscribe(this);
_animationTrigger?.OnNext(_activator.GetIsActive());
}
}
void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value);
_isActive = _activator?.GetIsActive() ?? true;
hasChanged = _isActive != previous;
return _isActive;
}
}
}

4
src/Avalonia.Base/Styling/TemplateSelector.cs

@ -26,11 +26,11 @@ namespace Avalonia.Styling
/// <inheritdoc/>
public override Type? TargetType => null;
public override string ToString()
public override string ToString(Style? owner)
{
if (_selectorString == null)
{
_selectorString = _parent.ToString() + " /template/ ";
_selectorString = _parent.ToString(owner) + " /template/ ";
}
return _selectorString;

8
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -84,11 +84,11 @@ namespace Avalonia.Styling
public IList<string> Classes => _classes.Value;
/// <inheritdoc/>
public override string ToString()
public override string ToString(Style? owner)
{
if (_selectorString == null)
{
_selectorString = BuildSelectorString();
_selectorString = BuildSelectorString(owner);
}
return _selectorString;
@ -143,13 +143,13 @@ namespace Avalonia.Styling
protected override Selector? MovePrevious() => _previous;
protected override Selector? MovePreviousOrParent() => _previous;
private string BuildSelectorString()
private string BuildSelectorString(Style? owner)
{
var builder = StringBuilderCache.Acquire();
if (_previous != null)
{
builder.Append(_previous.ToString());
builder.Append(_previous.ToString(owner));
}
if (TargetType != null)

368
src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs

@ -0,0 +1,368 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Utilities
{
/// <summary>
/// Stores values with <see cref="AvaloniaProperty"/> as key.
/// </summary>
/// <typeparam name="TValue">Stored value type.</typeparam>
/// <remarks>
/// This struct implements the most commonly-used part of the dictionary API, but does
/// not implement <see cref="IDictionary{TKey, TValue}"/>. In particular, this struct
/// is not enumerable. Enumeration is intended to be done by index for better performance.
/// </remarks>
internal struct AvaloniaPropertyDictionary<TValue>
{
private const int DefaultInitialCapacity = 4;
private Entry[]? _entries;
private int _entryCount;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaPropertyDictionary{TValue}"/>
/// class that is empty and has the default initial capacity.
/// </summary>
public AvaloniaPropertyDictionary()
{
_entries = null;
_entryCount = 0;
}
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaPropertyDictionary{TValue}"/>
/// class that is empty and has the specified initial capacity.
/// </summary>
/// <param name="capactity">
/// The initial number of elements that the collection can contain.
/// </param>
public AvaloniaPropertyDictionary(int capactity)
{
_entries = new Entry[capactity];
_entryCount = 0;
}
/// <summary>
/// Gets the number of key/value pairs contained in the collection.
/// </summary>
public int Count => _entryCount;
/// <summary>
/// Gets or sets the value associated with the specified key.
/// </summary>
/// <param name="property">The key to get or set.</param>
/// <returns>
/// The value associated with the specified key. If the key is not found, a get operation
/// throws a <see cref="KeyNotFoundException"/>, and a set operation creates a
/// new element for the specified key.
/// </returns>
/// <exception cref="KeyNotFoundException">
/// The key does not exist in the collection.
/// </exception>
public TValue this[AvaloniaProperty property]
{
get
{
if (!TryGetEntry(property.Id, out var index))
ThrowNotFound();
return _entries[index].Value;
}
set
{
if (TryGetEntry(property.Id, out var index))
_entries[index] = new Entry(property, value);
else
InsertEntry(new Entry(property, value), index);
}
}
/// <summary>
/// Gets the value at the specified index.
/// </summary>
/// <param name="index">
/// The index of the entry, between 0 and <see cref="Count"/> - 1.
/// </param>
public TValue this[int index]
{
get
{
if (index >= _entryCount)
ThrowOutOfRange();
return _entries![index].Value;
}
}
/// <summary>
/// Adds the specified key and value to the dictionary.
/// </summary>
/// <param name="property">The key.</param>
/// <param name="value">The value of the element to add.</param>
public void Add(AvaloniaProperty property, TValue value)
{
if (TryGetEntry(property.Id, out var index))
ThrowDuplicate();
InsertEntry(new Entry(property, value), index);
}
/// <summary>
/// Removes all keys and values from the collection.
/// </summary>
/// <remarks>
/// The Count property is set to 0, and references to other objects from elements of the
/// collection are also released. The capacity remains unchanged.
/// </remarks>
public void Clear()
{
if (_entries is not null)
{
Array.Clear(_entries, 0, _entries.Length);
_entryCount = 0;
}
}
/// <summary>
/// Determines whether the collection contains the specified key.
/// </summary>
/// <param name="property">The key.</param>
public bool ContainsKey(AvaloniaProperty property) => TryGetEntry(property.Id, out _);
/// <summary>
/// Gets the key and value at the specified index.
/// </summary>
/// <param name="index">
/// The index of the entry, between 0 and <see cref="Count"/> - 1.
/// </param>
/// <param name="key">
/// When this method returns, contains the key at the specified index.
/// </param>
/// <param name="value">
/// When this method returns, contains the value at the specified index.
/// </param>
public void GetKeyValue(int index, out AvaloniaProperty key, out TValue value)
{
if (index >= _entryCount)
ThrowOutOfRange();
ref var entry = ref _entries![index];
key = entry.Property;
value = entry.Value;
}
/// <summary>
/// Removes the value of the specified key from the collection.
/// </summary>
/// <param name="property">The key.</param>
/// <returns>
/// true if the element is successfully found and removed; otherwise, false. This method
/// returns false if key is not found in the collection.
/// </returns>
public bool Remove(AvaloniaProperty property)
{
if (TryGetEntry(property.Id, out var index))
{
RemoveAt(index);
return true;
}
return false;
}
/// <summary>
/// Removes the value of the specified key from the collection, and copies the element to
/// the value parameter.
/// </summary>
/// <param name="property">The key.</param>
/// <param name="value">The removed element.</param>
/// <returns>
/// true if the element is successfully found and removed; otherwise, false. This method
/// returns false if key is not found in the collection.
/// </returns>
public bool Remove(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
if (TryGetEntry(property.Id, out var index))
{
value = _entries[index].Value;
RemoveAt(index);
return true;
}
value = default;
return false;
}
/// <summary>
/// Removes the element at the specified index from the collection.
/// </summary>
/// <param name="index">The index.</param>
public void RemoveAt(int index)
{
if (_entries is null)
throw new IndexOutOfRangeException();
Array.Copy(_entries, index + 1, _entries, index, _entryCount - index - 1);
_entryCount--;
_entries[_entryCount] = default;
}
/// <summary>
/// Attempts to add the specified key and value to the collection.
/// </summary>
/// <param name="property">The key.</param>
/// <param name="value">The value of the element to add.</param>
/// <returns></returns>
public bool TryAdd(AvaloniaProperty property, TValue value)
{
if (TryGetEntry(property.Id, out var index))
return false;
InsertEntry(new Entry(property, value), index);
return true;
}
/// <summary>
/// Gets the value associated with the specified key.
/// </summary>
/// <param name="property">The property key.</param>
/// <param name="value">
/// When this method returns, contains the value associated with the specified key,
/// if the property is found; otherwise, null. This parameter is passed uninitialized.
/// </param>
/// <returns></returns>
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
if (TryGetEntry(property.Id, out var index))
{
value = _entries[index].Value;
return true;
}
value = default;
return false;
}
[MemberNotNullWhen(true, nameof(_entries))]
private bool TryGetEntry(int propertyId, out int index)
{
int checkIndex;
int iLo = 0;
int iHi = _entryCount;
if (iHi <= 0)
{
index = 0;
return false;
}
// Do a binary search to find the value
while (iHi - iLo > 3)
{
int iPv = (iHi + iLo) / 2;
checkIndex = _entries![iPv].Property.Id;
if (propertyId == checkIndex)
{
index = iPv;
return true;
}
if (propertyId <= checkIndex)
{
iHi = iPv;
}
else
{
iLo = iPv + 1;
}
}
// Now we only have three values to search; switch to a linear search
do
{
checkIndex = _entries![iLo].Property.Id;
if (checkIndex == propertyId)
{
index = iLo;
return true;
}
if (checkIndex > propertyId)
{
// we've gone past the targetIndex - return not found
break;
}
iLo++;
} while (iLo < iHi);
index = iLo;
return false;
}
[MemberNotNull(nameof(_entries))]
private void InsertEntry(Entry entry, int entryIndex)
{
if (_entryCount > 0)
{
if (_entryCount == _entries!.Length)
{
const double growthFactor = 1.2;
var newSize = (int)(_entryCount * growthFactor);
if (newSize == _entryCount)
{
newSize++;
}
var destEntries = new Entry[newSize];
Array.Copy(_entries, 0, destEntries, 0, entryIndex);
destEntries[entryIndex] = entry;
Array.Copy(_entries, entryIndex, destEntries, entryIndex + 1, _entryCount - entryIndex);
_entries = destEntries;
}
else
{
Array.Copy(
_entries,
entryIndex,
_entries,
entryIndex + 1,
_entryCount - entryIndex);
_entries[entryIndex] = entry;
}
}
else
{
_entries ??= new Entry[DefaultInitialCapacity];
_entries[0] = entry;
}
_entryCount++;
}
[DoesNotReturn]
private static void ThrowOutOfRange() => throw new IndexOutOfRangeException();
[DoesNotReturn]
private static void ThrowDuplicate() =>
throw new ArgumentException("An item with the same key has already been added.");
[DoesNotReturn]
private static void ThrowNotFound() => throw new KeyNotFoundException();
private readonly struct Entry
{
public readonly AvaloniaProperty Property;
public readonly TValue Value;
public Entry(AvaloniaProperty property, TValue value)
{
Property = property;
Value = value;
}
}
}
}

173
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -1,173 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Utilities
{
/// <summary>
/// Stores values with <see cref="AvaloniaProperty"/> as key.
/// </summary>
/// <typeparam name="TValue">Stored value type.</typeparam>
internal sealed class AvaloniaPropertyValueStore<TValue>
{
// The last item in the list is always int.MaxValue.
private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } };
private Entry[] _entries;
public AvaloniaPropertyValueStore()
{
_entries = s_emptyEntries;
}
public int Count => _entries.Length - 1;
public TValue this[int index] => _entries[index].Value;
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId)
return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId)
return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId)
return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId)
return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId)
return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId)
return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId)
return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId)
return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId)
return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId)
return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId)
return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
}
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = default;
return false;
}
value = _entries[index].Value;
return true;
}
public void AddValue(AvaloniaProperty property, TValue value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_entries = entries;
}
public void SetValue(AvaloniaProperty property, TValue value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
public void Remove(AvaloniaProperty property)
{
var (index, found) = TryFindEntry(property.Id);
if (found)
{
var newLength = _entries.Length - 1;
// Special case - one element left means that value store is empty so we can just reuse our "empty" array.
if (newLength == 1)
{
_entries = s_emptyEntries;
return;
}
var entries = new Entry[newLength];
int ix = 0;
for (int i = 0; i < _entries.Length; ++i)
{
if (i != index)
{
entries[ix++] = _entries[i];
}
}
_entries = entries;
}
}
private struct Entry
{
internal int PropertyId;
internal TValue Value;
}
}
}

507
src/Avalonia.Base/ValueStore.cs

@ -1,507 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
namespace Avalonia
{
/// <summary>
/// Stores styled property values for an <see cref="AvaloniaObject"/>.
/// </summary>
/// <remarks>
/// At its core this class consists of an <see cref="AvaloniaProperty"/> to
/// <see cref="IValue"/> mapping which holds the current values for each set property. This
/// <see cref="IValue"/> can be in one of 4 states:
///
/// - For a single local value it will be an instance of <see cref="LocalValueEntry{T}"/>.
/// - For a single value of a priority other than LocalValue it will be an instance of
/// <see cref="ConstantValueEntry{T}"/>`
/// - For a single binding it will be an instance of <see cref="BindingEntry{T}"/>
/// - For all other cases it will be an instance of <see cref="PriorityValue{T}"/>
/// </remarks>
internal class ValueStore
{
private readonly AvaloniaObject _owner;
private readonly AvaloniaPropertyValueStore<IValue> _values;
private BatchUpdate? _batchUpdate;
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
_values = new AvaloniaPropertyValueStore<IValue>();
}
public void BeginBatchUpdate()
{
_batchUpdate ??= new BatchUpdate(this);
_batchUpdate.Begin();
}
public void EndBatchUpdate()
{
if (_batchUpdate is null)
{
throw new InvalidOperationException("No batch update in progress.");
}
if (_batchUpdate.End())
{
_batchUpdate = null;
}
}
public bool IsAnimating(AvaloniaProperty property)
{
if (TryGetValue(property, out var slot))
{
return slot.Priority < BindingPriority.LocalValue;
}
return false;
}
public bool IsSet(AvaloniaProperty property)
{
if (TryGetValue(property, out var slot))
{
return slot.GetValue().HasValue;
}
return false;
}
public bool TryGetValue<T>(
StyledPropertyBase<T> property,
BindingPriority maxPriority,
out T value)
{
if (TryGetValue(property, out var slot))
{
var v = ((IValue<T>)slot).GetValue(maxPriority);
if (v.HasValue)
{
value = v.Value;
return true;
}
}
value = default!;
return false;
}
public IDisposable? SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
{
if (property.ValidateValue?.Invoke(value) == false)
{
throw new ArgumentException($"{value} is not a valid value for '{property.Name}.");
}
IDisposable? result = null;
if (TryGetValue(property, out var slot))
{
result = SetExisting(slot, property, value, priority);
}
else if (property.HasCoercion)
{
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this);
AddValue(property, entry);
result = entry.SetValue(value, priority);
}
else
{
if (priority == BindingPriority.LocalValue)
{
AddValue(property, new LocalValueEntry<T>(value));
NotifyValueChanged<T>(property, default, value, priority);
}
else
{
var entry = new ConstantValueEntry<T>(property, value, priority, new(this));
AddValue(property, entry);
NotifyValueChanged<T>(property, default, value, priority);
result = entry;
}
}
return result;
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
if (TryGetValue(property, out var slot))
{
return BindExisting(slot, property, source, priority);
}
else if (property.HasCoercion)
{
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this);
var binding = entry.AddBinding(source, priority);
AddValue(property, entry);
return binding;
}
else
{
var entry = new BindingEntry<T>(_owner, property, source, priority, new(this));
AddValue(property, entry);
return entry;
}
}
public void ClearLocalValue<T>(StyledPropertyBase<T> property)
{
if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue<T> p)
{
p.ClearLocalValue();
}
else if (slot.Priority == BindingPriority.LocalValue)
{
var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ?
new Optional<T>(value) : default;
// During batch update values can't be removed immediately because they're needed to raise
// a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
// by setting their priority to Unset.
if (!IsBatchUpdating())
{
_values.Remove(property);
}
else if (slot is IDisposable d)
{
d.Dispose();
}
else
{
// Local value entries are optimized and contain only a single value field to save space,
// so there's no way to mark them for removal at the end of a batch update. Instead convert
// them to a constant value entry with Unset priority in the event of a local value being
// cleared during a batch update.
var sentinel = new ConstantValueEntry<T>(property, Optional<T>.Empty, BindingPriority.Unset, new(this));
_values.SetValue(property, sentinel);
}
NotifyValueChanged<T>(property, old, default, BindingPriority.Unset);
}
}
}
public void CoerceValue(AvaloniaProperty property)
{
if (TryGetValue(property, out var slot))
{
if (slot is IPriorityValue p)
{
p.UpdateEffectiveValue();
}
}
}
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
{
if (TryGetValue(property, out var slot))
{
var slotValue = slot.GetValue();
return new Diagnostics.AvaloniaPropertyValue(
property,
slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue,
slot.Priority,
null);
}
return null;
}
public void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (_batchUpdate is object)
{
if (change.IsEffectiveValueChange)
{
NotifyValueChanged<T>(change.Property, change.OldValue, change.NewValue, change.Priority);
}
}
else
{
_owner.ValueChanged(change);
}
}
public void Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue)
{
// We need to include remove sentinels here so call `_values.TryGetValue` directly.
if (_values.TryGetValue(property, out var slot) && slot == entry)
{
if (_batchUpdate is null)
{
_values.Remove(property);
_owner.Completed(property, entry, oldValue);
}
else
{
_batchUpdate.ValueChanged(property, oldValue.ToObject());
}
}
}
private IDisposable? SetExisting<T>(
object slot,
StyledPropertyBase<T> property,
T value,
BindingPriority priority)
{
IDisposable? result = null;
if (slot is IPriorityValueEntry<T> e)
{
var priorityValue = new PriorityValue<T>(_owner, property, this, e);
_values.SetValue(property, priorityValue);
result = priorityValue.SetValue(value, priority);
}
else if (slot is PriorityValue<T> p)
{
result = p.SetValue(value, priority);
}
else if (slot is LocalValueEntry<T> l)
{
if (priority == BindingPriority.LocalValue)
{
var old = l.GetValue(BindingPriority.LocalValue);
l.SetValue(value);
NotifyValueChanged<T>(property, old, value, priority);
}
else
{
var priorityValue = new PriorityValue<T>(_owner, property, this, l);
if (IsBatchUpdating())
priorityValue.BeginBatchUpdate();
result = priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue);
}
}
else
{
throw new NotSupportedException("Unrecognised value store slot type.");
}
return result;
}
private IDisposable BindExisting<T>(
object slot,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
PriorityValue<T> priorityValue;
if (slot is IPriorityValueEntry<T> e)
{
priorityValue = new PriorityValue<T>(_owner, property, this, e);
if (IsBatchUpdating())
{
priorityValue.BeginBatchUpdate();
}
}
else if (slot is PriorityValue<T> p)
{
priorityValue = p;
}
else if (slot is LocalValueEntry<T> l)
{
priorityValue = new PriorityValue<T>(_owner, property, this, l);
}
else
{
throw new NotSupportedException("Unrecognised value store slot type.");
}
var binding = priorityValue.AddBinding(source, priority);
_values.SetValue(property, priorityValue);
priorityValue.UpdateEffectiveValue();
return binding;
}
private void AddValue(AvaloniaProperty property, IValue value)
{
_values.AddValue(property, value);
if (IsBatchUpdating() && value is IBatchUpdate batch)
batch.BeginBatchUpdate();
value.Start();
}
private void NotifyValueChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
if (_batchUpdate is null)
{
_owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
oldValue,
newValue,
priority));
}
else
{
_batchUpdate.ValueChanged(property, oldValue.ToObject());
}
}
private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true;
private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value)
{
return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value);
}
private static bool IsRemoveSentinel(IValue value)
{
// Local value entries are optimized and contain only a single value field to save space,
// so there's no way to mark them for removal at the end of a batch update. Instead a
// ConstantValueEntry with a priority of Unset is used as a sentinel value.
return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset;
}
private class BatchUpdate
{
private ValueStore _owner;
private List<Notification>? _notifications;
private int _batchUpdateCount;
private int _iterator = -1;
public BatchUpdate(ValueStore owner) => _owner = owner;
public bool IsBatchUpdating => _batchUpdateCount > 0;
public void Begin()
{
if (_batchUpdateCount++ == 0)
{
var values = _owner._values;
for (var i = 0; i < values.Count; ++i)
{
(values[i] as IBatchUpdate)?.BeginBatchUpdate();
}
}
}
public bool End()
{
if (--_batchUpdateCount > 0)
return false;
var values = _owner._values;
// First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
// but notifications will still not be raised because the owner ValueStore will still have a reference
// to this batch update object.
for (var i = 0; i < values.Count; ++i)
{
(values[i] as IBatchUpdate)?.EndBatchUpdate();
// Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it
// does, abort and continue batch updating.
if (_batchUpdateCount > 0)
return false;
}
if (_notifications is object)
{
// Raise all batched notifications. Doing this can cause other notifications to be added and even
// cause a new batch update to start, so we need to handle _notifications being modified by storing
// the index in field.
_iterator = 0;
for (; _iterator < _notifications.Count; ++_iterator)
{
var entry = _notifications[_iterator];
if (values.TryGetValue(entry.property, out var slot))
{
var oldValue = entry.oldValue;
var newValue = slot.GetValue();
// Raising this notification can cause a new batch update to be started, which in turn
// results in another change to the property. In this case we need to update the old value
// so that the *next* notification has an oldValue which follows on from the newValue
// raised here.
_notifications[_iterator] = new Notification
{
property = entry.property,
oldValue = newValue,
};
// Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs<T>.
slot.RaiseValueChanged(_owner._owner, entry.property, oldValue, newValue);
// During batch update values can't be removed immediately because they're needed to raise
// the _sink.ValueChanged notification. They instead mark themselves for removal by setting
// their priority to Unset. We need to re-read the slot here because raising ValueChanged
// could have caused it to be updated.
if (values.TryGetValue(entry.property, out var updatedSlot) &&
updatedSlot.Priority == BindingPriority.Unset)
{
values.Remove(entry.property);
}
}
// If a new batch update was started while ending this one, abort.
if (_batchUpdateCount > 0)
return false;
}
}
_iterator = int.MaxValue - 1;
return true;
}
public void ValueChanged(AvaloniaProperty property, Optional<object?> oldValue)
{
_notifications ??= new List<Notification>();
for (var i = 0; i < _notifications.Count; ++i)
{
if (_notifications[i].property == property)
{
oldValue = _notifications[i].oldValue;
_notifications.RemoveAt(i);
if (i <= _iterator)
--_iterator;
break;
}
}
_notifications.Add(new Notification
{
property = property,
oldValue = oldValue,
});
}
private struct Notification
{
public AvaloniaProperty property;
public Optional<object?> oldValue;
}
}
}
}

33
src/Avalonia.Base/Visual.cs

@ -536,27 +536,24 @@ namespace Avalonia
BindingPriority.LocalValue);
}
protected internal sealed override void LogBindingError(AvaloniaProperty property, Exception e)
internal override ParametrizedLogger? GetBindingWarningLogger(
AvaloniaProperty property,
Exception? e)
{
// Don't log a binding error unless the control is attached to a logical tree.
if (((ILogical)this).IsAttachedToLogicalTree)
{
if (e is BindingChainException b &&
string.IsNullOrEmpty(b.ExpressionErrorPoint) &&
DataContext == null)
{
// The error occurred at the root of the binding chain and DataContext is null;
// don't log this - the DataContext probably hasn't been set up yet.
return;
}
// Don't log a binding error unless the control is attached to the logical tree.
if (!((ILogical)this).IsAttachedToLogicalTree)
return null;
Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log(
this,
"Error in binding to {Target}.{Property}: {Message}",
this,
property,
e.Message);
if (e is BindingChainException b &&
string.IsNullOrEmpty(b.ExpressionErrorPoint) &&
DataContext == null)
{
// The error occurred at the root of the binding chain and DataContext is null;
// don't log this - the DataContext probably hasn't been set up yet.
return null;
}
return Logger.TryGet(LogEventLevel.Warning, LogArea.Binding);
}
/// <summary>

64
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -6,6 +6,7 @@ using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
@ -364,17 +365,6 @@ namespace Avalonia.Controls.Primitives
{
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
{
foreach (var child in this.GetTemplateChildren())
child.InvalidateStyles();
}
}
/// <summary>
/// Called when the <see cref="Template"/> property changes.
/// </summary>
@ -404,5 +394,57 @@ namespace Avalonia.Controls.Primitives
}
}
}
internal override void DetachControlThemeFromTemplateChildren(ControlTheme theme)
{
static ControlTheme? GetControlTheme(StyleBase style)
{
var s = style;
while (s is not null)
{
if (s is ControlTheme c)
return c;
s = s.Parent as StyleBase;
}
return null;
}
static void Detach(Visual control, ITemplatedControl templatedParent, ControlTheme theme)
{
var valueStore = control.GetValueStore();
var count = valueStore.Frames.Count;
if (control != templatedParent)
{
valueStore.BeginStyling();
for (var i = count - 1; i >= 0; --i)
{
if (valueStore.Frames[i] is StyleInstance si &&
si.Source is StyleBase style &&
GetControlTheme(style) == theme)
{
valueStore.RemoveFrame(si);
}
}
valueStore.EndStyling();
}
var children = ((IVisual)control).VisualChildren;
count = children.Count;
for (var i = 0; i < count; i++)
{
if (children[i] is Visual v &&
v.TemplatedParent == templatedParent)
Detach(v, templatedParent, theme);
}
}
Detach(this, this, theme);
}
}
}

1
src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml

@ -70,7 +70,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type ButtonSpinner}" TargetType="ButtonSpinner">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Padding" Value="10, 0" />
<Setter Property="Background" Value="{DynamicResource TextControlBackground}" />

75
tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs

@ -413,25 +413,86 @@ namespace Avalonia.Base.UnitTests.Animation
}
[Fact]
public void Transitions_Can_Re_Set_During_Batch_Update()
public void Transitions_Can_Re_Set_During_Styling()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
// Assigning and then clearing Transitions ensures we have a transition state
// collection created.
control.Transitions = null;
control.ClearValue(Control.TransitionsProperty);
control.BeginBatchUpdate();
control.GetValueStore().BeginStyling();
// Setting opacity then Transitions means that we receive the Transitions change
// after the Opacity change when EndBatchUpdate is called.
control.Opacity = 0.5;
control.Transitions = new Transitions { target.Object };
// after the Opacity change when EndStyling is called.
var style = new Style
{
Setters =
{
new Setter(Control.OpacityProperty, 0.5),
new Setter(Control.TransitionsProperty, new Transitions { target.Object }),
}
};
style.TryAttach(control, control);
// Which means that the transition state hasn't been initialized with the new
// Transitions when the Opacity change notification gets raised here.
control.EndBatchUpdate();
control.GetValueStore().EndStyling();
}
[Fact]
public void Transitions_Can_Be_Removed_While_Transition_In_Progress()
{
using var app = Start();
var opacityTransition = new DoubleTransition
{
Property = Control.OpacityProperty,
Duration = TimeSpan.FromSeconds(1),
};
var transitions = new Transitions { opacityTransition };
var borderTheme = new ControlTheme(typeof(Border))
{
Setters =
{
new Setter(Control.TransitionsProperty, transitions),
}
};
var clock = new TestClock();
var root = new TestRoot
{
Clock = clock,
Resources =
{
{ typeof(Border), borderTheme },
}
};
var border = new Border();
root.Child = border;
root.LayoutManager.ExecuteInitialLayoutPass();
Assert.Same(transitions, border.Transitions);
// First set property with a transition to a new value, and step the clock until
// transition is complete.
border.Opacity = 0;
clock.Step(TimeSpan.FromSeconds(0));
clock.Step(TimeSpan.FromSeconds(1));
Assert.Equal(0, border.Opacity);
// Now clear the property; a transition is now in progress but no local value is
// set.
border.ClearValue(Border.OpacityProperty);
// Remove the transition by removing the control from the logical tree. This was
// causing an exception.
root.Child = null;
}
private static IDisposable Start()

695
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@ -1,695 +0,0 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Data;
using Avalonia.Layout;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_BatchUpdate
{
[Fact]
public void SetValue_Should_Not_Raise_Property_Changes_During_Batch_Update()
{
var target = new TestClass();
var raised = new List<string>();
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
Assert.Empty(raised);
}
[Fact]
public void Binding_Should_Not_Raise_Property_Changes_During_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<string>();
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
Assert.Empty(raised);
}
[Fact]
public void Binding_Completion_Should_Not_Raise_Property_Changes_During_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<string>();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
target.BeginBatchUpdate();
observable.OnCompleted();
Assert.Empty(raised);
}
[Fact]
public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<string>();
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
target.BeginBatchUpdate();
sub.Dispose();
Assert.Empty(raised);
}
[Fact]
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal("foo", target.Foo);
Assert.Null(raised[0].OldValue);
Assert.Equal("foo", raised[0].NewValue);
}
[Fact]
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_2()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "bar", BindingPriority.LocalValue);
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal("baz", target.Foo);
}
[Fact]
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_3()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.BazProperty, Orientation.Horizontal, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal(TestClass.BazProperty, raised[0].Property);
Assert.Equal(Orientation.Vertical, raised[0].OldValue);
Assert.Equal(Orientation.Horizontal, raised[0].NewValue);
Assert.Equal(Orientation.Horizontal, target.Baz);
}
[Fact]
public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.BarProperty, raised[0].Property);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("baz", target.Foo);
Assert.Equal("bar", target.Bar);
}
[Fact]
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_1()
{
var target = new TestClass();
var observable = new TestObservable<string>("baz");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.BarProperty, raised[0].Property);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("baz", target.Foo);
Assert.Equal("bar", target.Bar);
}
[Fact]
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_2()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.BarProperty, raised[0].Property);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("baz", target.Foo);
Assert.Equal("bar", target.Bar);
}
[Fact]
public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_3()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("qux");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.BarProperty, raised[0].Property);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("baz", target.Foo);
Assert.Equal("bar", target.Bar);
}
[Fact]
public void Binding_Change_Should_Be_Raised_After_Batch_Update_1()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal("foo", target.Foo);
Assert.Null(raised[0].OldValue);
Assert.Equal("foo", raised[0].NewValue);
}
[Fact]
public void Binding_Change_Should_Be_Raised_After_Batch_Update_2()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("bar");
var observable2 = new TestObservable<string>("baz");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal("baz", target.Foo);
Assert.Equal("foo", raised[0].OldValue);
Assert.Equal("baz", raised[0].NewValue);
}
[Fact]
public void Binding_Change_Should_Be_Raised_After_Batch_Update_3()
{
var target = new TestClass();
var observable = new TestObservable<Orientation>(Orientation.Horizontal);
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Bind(TestClass.BazProperty, observable, BindingPriority.LocalValue);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Equal(TestClass.BazProperty, raised[0].Property);
Assert.Equal(Orientation.Vertical, raised[0].OldValue);
Assert.Equal(Orientation.Horizontal, raised[0].NewValue);
Assert.Equal(Orientation.Horizontal, target.Baz);
}
[Fact]
public void Binding_Completion_Should_Be_Raised_After_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
observable.OnCompleted();
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Null(target.Foo);
Assert.Equal("foo", raised[0].OldValue);
Assert.Null(raised[0].NewValue);
Assert.Equal(BindingPriority.Unset, raised[0].Priority);
}
[Fact]
public void Binding_Disposal_Should_Be_Raised_After_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
sub.Dispose();
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Null(target.Foo);
Assert.Equal("foo", raised[0].OldValue);
Assert.Null(raised[0].NewValue);
Assert.Equal(BindingPriority.Unset, raised[0].Priority);
}
[Fact]
public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.Foo = "foo";
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Null(target.Foo);
Assert.Equal("foo", raised[0].OldValue);
Assert.Null(raised[0].NewValue);
Assert.Equal(BindingPriority.Unset, raised[0].Priority);
}
[Fact]
public void Bindings_Should_Be_Subscribed_Before_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(1, observable2.SubscribeCount);
}
[Fact]
public void Non_Active_Binding_Should_Not_Be_Subscribed_Before_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(0, observable2.SubscribeCount);
}
[Fact]
public void LocalValue_Bindings_Should_Be_Subscribed_During_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
// We need to subscribe to LocalValue bindings even if we've got a batch operation
// in progress because otherwise we don't know whether the binding or a subsequent
// SetValue with local priority will win. Notifications however shouldn't be sent.
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(1, observable2.SubscribeCount);
Assert.Empty(raised);
}
[Fact]
public void Style_Bindings_Should_Not_Be_Subscribed_During_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.StyleTrigger);
Assert.Equal(0, observable1.SubscribeCount);
Assert.Equal(0, observable2.SubscribeCount);
}
[Fact]
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_1()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
target.EndBatchUpdate();
Assert.Equal(0, observable1.SubscribeCount);
Assert.Equal(1, observable2.SubscribeCount);
}
[Fact]
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_2()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.StyleTrigger);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
target.EndBatchUpdate();
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(0, observable2.SubscribeCount);
}
[Fact]
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_1()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Foo = "foo";
target.PropertyChanged += (s, e) =>
{
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
target.Bar = "bar";
};
target.EndBatchUpdate();
Assert.Equal("foo", target.Foo);
Assert.Equal("bar", target.Bar);
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.FooProperty, raised[0].Property);
Assert.Equal(TestClass.BarProperty, raised[1].Property);
}
[Fact]
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_2()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Foo = "foo";
target.Bar = "baz";
target.PropertyChanged += (s, e) =>
{
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
target.Bar = "bar";
};
target.EndBatchUpdate();
Assert.Equal("foo", target.Foo);
Assert.Equal("bar", target.Bar);
Assert.Equal(2, raised.Count);
}
[Fact]
public void Batch_Update_Can_Be_Triggered_By_Ending_Batch_Update()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Foo = "foo";
target.Bar = "baz";
// Simulates the following scenario:
// - A control is added to the logical tree
// - A batch update is started to apply styles
// - Ending the batch update triggers something which removes the control from the logical tree
// - A new batch update is started to detach styles
target.PropertyChanged += (s, e) =>
{
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
{
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.ClearValue(TestClass.BarProperty);
target.EndBatchUpdate();
}
};
target.EndBatchUpdate();
Assert.Null(target.Foo);
Assert.Null(target.Bar);
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.FooProperty, raised[0].Property);
Assert.Null(raised[0].OldValue);
Assert.Equal("foo", raised[0].NewValue);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("foo", raised[1].OldValue);
Assert.Null(raised[1].NewValue);
}
[Fact]
public void Can_Set_Cleared_Value_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
target.Foo = "foo";
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.FooProperty && e.NewValue is null)
{
target.Foo = "bar";
++raised;
}
};
target.EndBatchUpdate();
Assert.Equal("bar", target.Foo);
Assert.Equal(1, raised);
}
[Fact]
public void Can_Bind_Cleared_Value_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
var notifications = new List<AvaloniaPropertyChangedEventArgs>();
target.Foo = "foo";
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.FooProperty && e.NewValue is null)
{
target.Bind(TestClass.FooProperty, new TestObservable<string>("bar"));
++raised;
}
notifications.Add(e);
};
target.EndBatchUpdate();
Assert.Equal("bar", target.Foo);
Assert.Equal(1, raised);
Assert.Equal(2, notifications.Count);
Assert.Equal(null, notifications[0].NewValue);
Assert.Equal("bar", notifications[1].NewValue);
}
[Fact]
public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
var notifications = new List<AvaloniaPropertyChangedEventArgs>();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("foo");
target.Bind(TestClass.FooProperty, observable1);
target.BeginBatchUpdate();
observable1.OnCompleted();
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.FooProperty && e.NewValue is null)
{
target.Bind(TestClass.FooProperty, observable2);
++raised;
}
notifications.Add(e);
};
target.EndBatchUpdate();
Assert.Equal("foo", target.Foo);
Assert.Equal(1, raised);
Assert.Equal(2, notifications.Count);
Assert.Equal(null, notifications[0].NewValue);
Assert.Equal("foo", notifications[1].NewValue);
}
[Fact]
public void Can_Run_Empty_Batch_Update_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
var notifications = new List<AvaloniaPropertyChangedEventArgs>();
target.Foo = "foo";
target.Bar = "bar";
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.ClearValue(TestClass.BarProperty);
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.BarProperty)
{
target.BeginBatchUpdate();
target.EndBatchUpdate();
}
++raised;
};
target.EndBatchUpdate();
Assert.Null(target.Foo);
Assert.Null(target.Bar);
Assert.Equal(2, raised);
}
public class TestClass : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<TestClass, string>(nameof(Foo));
public static readonly StyledProperty<string> BarProperty =
AvaloniaProperty.Register<TestClass, string>(nameof(Bar));
public static readonly StyledProperty<Orientation> BazProperty =
AvaloniaProperty.Register<TestClass, Orientation>(nameof(Bar), Orientation.Vertical);
public string Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
public string Bar
{
get => GetValue(BarProperty);
set => SetValue(BarProperty, value);
}
public Orientation Baz
{
get => GetValue(BazProperty);
set => SetValue(BazProperty, value);
}
}
public class TestObservable<T> : ObservableBase<BindingValue<T>>
{
private readonly T _value;
private IObserver<BindingValue<T>> _observer;
public TestObservable(T value) => _value = value;
public int SubscribeCount { get; private set; }
public void OnCompleted() => _observer.OnCompleted();
public void OnError(Exception e) => _observer.OnError(e);
protected override IDisposable SubscribeCore(IObserver<BindingValue<T>> observer)
{
++SubscribeCount;
_observer = observer;
observer.OnNext(_value);
return Disposable.Empty;
}
}
}
}

559
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -4,18 +4,18 @@ using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Base.UnitTests.Styling;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using Microsoft.Reactive.Testing;
using Moq;
using Xunit;
#nullable enable
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_Binding
@ -24,11 +24,10 @@ namespace Avalonia.Base.UnitTests
public void Bind_Sets_Current_Value()
{
var target = new Class1();
var source = new Class1();
var source = new BehaviorSubject<BindingValue<string>>("initial");
var property = Class1.FooProperty;
source.SetValue(property, "initial");
target.Bind(property, source.GetObservable(property));
target.Bind(property, source);
Assert.Equal("initial", target.GetValue(property));
}
@ -38,18 +37,21 @@ namespace Avalonia.Base.UnitTests
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
bool raised = false;
var raised = 0;
target.PropertyChanged += (s, e) =>
raised = e.Property == Class1.FooProperty &&
(string)e.OldValue == "foodefault" &&
(string)e.NewValue == "newvalue" &&
e.Priority == BindingPriority.LocalValue;
{
Assert.Equal(Class1.FooProperty, e.Property);
Assert.Equal("foodefault", (string?)e.OldValue);
Assert.Equal("newvalue", (string?)e.NewValue);
Assert.Equal(BindingPriority.LocalValue, e.Priority);
++raised;
};
target.Bind(Class1.FooProperty, source);
source.OnNext("newvalue");
Assert.True(raised);
Assert.Equal(1, raised);
}
[Fact]
@ -71,7 +73,7 @@ namespace Avalonia.Base.UnitTests
public void Setting_LocalValue_Overrides_Binding_Until_Binding_Produces_Next_Value()
{
var target = new Class1();
var source = new Subject<string>();
var source = new Subject<BindingValue<string>>();
var property = Class1.FooProperty;
target.Bind(property, source);
@ -81,7 +83,7 @@ namespace Avalonia.Base.UnitTests
target.SetValue(property, "bar");
Assert.Equal("bar", target.GetValue(property));
source.OnNext("baz");
source.OnNext("baz");
Assert.Equal("baz", target.GetValue(property));
}
@ -89,7 +91,7 @@ namespace Avalonia.Base.UnitTests
public void Completing_LocalValue_Binding_Reverts_To_Default_Value_Even_When_Local_Value_Set_Earlier()
{
var target = new Class1();
var source = new Subject<string>();
var source = new Subject<BindingValue<string>>();
var property = Class1.FooProperty;
target.Bind(property, source);
@ -102,10 +104,10 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void Completing_LocalValue_Binding_Should_Not_Revert_To_Set_LocalValue()
public void Disposing_LocalValue_Binding_Should_Not_Revert_To_Set_LocalValue()
{
var target = new Class1();
var source = new BehaviorSubject<string>("bar");
var source = new BehaviorSubject<BindingValue<string>>("bar");
target.SetValue(Class1.FooProperty, "foo");
var sub = target.Bind(Class1.FooProperty, source);
@ -117,11 +119,43 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
}
[Fact]
public void LocalValue_Binding_Should_Override_Style_Binding()
{
var target = new Class1();
var source1 = new BehaviorSubject<BindingValue<string>>("foo");
var source2 = new BehaviorSubject<BindingValue<string>>("bar");
target.Bind(Class1.FooProperty, source1, BindingPriority.Style);
Assert.Equal("foo", target.GetValue(Class1.FooProperty));
target.Bind(Class1.FooProperty, source2, BindingPriority.LocalValue);
Assert.Equal("bar", target.GetValue(Class1.FooProperty));
}
[Fact]
public void Style_Binding_Should_NotOverride_LocalValue_Binding()
{
var target = new Class1();
var source1 = new BehaviorSubject<BindingValue<string>>("foo");
var source2 = new BehaviorSubject<BindingValue<string>>("bar");
target.Bind(Class1.FooProperty, source1, BindingPriority.LocalValue);
Assert.Equal("foo", target.GetValue(Class1.FooProperty));
target.Bind(Class1.FooProperty, source2, BindingPriority.Style);
Assert.Equal("foo", target.GetValue(Class1.FooProperty));
}
[Fact]
public void Completing_Animation_Binding_Reverts_To_Set_LocalValue()
{
var target = new Class1();
var source = new Subject<string>();
var source = new Subject<BindingValue<string>>();
var property = Class1.FooProperty;
target.SetValue(property, "foo");
@ -192,7 +226,7 @@ namespace Avalonia.Base.UnitTests
var property = Class1.FooProperty;
var raised = 0;
target.Bind(property, new BehaviorSubject<string>("bar"), BindingPriority.Style);
target.Bind(property, new BehaviorSubject<BindingValue<string>>("bar"), BindingPriority.Style);
target.Bind(property, source);
Assert.Equal("foo", target.GetValue(property));
@ -255,18 +289,18 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void Second_LocalValue_Binding_Overrides_First()
public void Second_LocalValue_Binding_Unsubscribes_First()
{
var property = Class1.FooProperty;
var target = new Class1();
var source1 = new Subject<string>();
var source2 = new Subject<string>();
var source1 = new Subject<BindingValue<string>>();
var source2 = new Subject<BindingValue<string>>();
target.Bind(property, source1, BindingPriority.LocalValue);
target.Bind(property, source2, BindingPriority.LocalValue);
source1.OnNext("foo");
Assert.Equal("foo", target.GetValue(property));
Assert.Equal("foodefault", target.GetValue(property));
source2.OnNext("bar");
Assert.Equal("bar", target.GetValue(property));
@ -276,12 +310,12 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void Completing_Second_LocalValue_Binding_Reverts_To_First()
public void Completing_Second_LocalValue_Binding_Doesnt_Revert_To_First()
{
var property = Class1.FooProperty;
var target = new Class1();
var source1 = new Subject<string>();
var source2 = new Subject<string>();
var source1 = new Subject<BindingValue<string>>();
var source2 = new Subject<BindingValue<string>>();
target.Bind(property, source1, BindingPriority.LocalValue);
target.Bind(property, source2, BindingPriority.LocalValue);
@ -291,7 +325,7 @@ namespace Avalonia.Base.UnitTests
source1.OnNext("baz");
source2.OnCompleted();
Assert.Equal("baz", target.GetValue(property));
Assert.Equal("foodefault", target.GetValue(property));
}
[Fact]
@ -299,8 +333,8 @@ namespace Avalonia.Base.UnitTests
{
var property = Class1.FooProperty;
var target = new Class1();
var source1 = new Subject<string>();
var source2 = new Subject<string>();
var source1 = new Subject<BindingValue<string>>();
var source2 = new Subject<BindingValue<string>>();
target.Bind(property, source1, BindingPriority.Style);
target.Bind(property, source2, BindingPriority.StyleTrigger);
@ -326,7 +360,19 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void Bind_To_ValueType_Accepts_UnsetValue()
public void Bind_NonGeneric_Can_Set_Null_On_Reference_Type()
{
var target = new Class1();
var source = new BehaviorSubject<object?>(null);
var property = Class1.FooProperty;
target.Bind(property, source);
Assert.Null(target.GetValue(property));
}
[Fact]
public void LocalValue_Bind_NonGeneric_To_ValueType_Accepts_UnsetValue()
{
var target = new Class1();
var source = new Subject<object>();
@ -339,6 +385,46 @@ namespace Avalonia.Base.UnitTests
Assert.False(target.IsSet(Class1.QuxProperty));
}
[Fact]
public void Style_Bind_NonGeneric_To_ValueType_Accepts_UnsetValue()
{
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, source, BindingPriority.Style);
source.OnNext(6.7);
source.OnNext(AvaloniaProperty.UnsetValue);
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
Assert.False(target.IsSet(Class1.QuxProperty));
}
[Fact]
public void LocalValue_Bind_NonGeneric_To_ValueType_Accepts_DoNothing()
{
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(BindingOperations.DoNothing);
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void Style_Bind_NonGeneric_To_ValueType_Accepts_DoNothing()
{
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, source, BindingPriority.Style);
source.OnNext(6.7);
source.OnNext(BindingOperations.DoNothing);
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void OneTime_Binding_Ignores_UnsetValue()
{
@ -374,7 +460,7 @@ namespace Avalonia.Base.UnitTests
{
Class1 target = new Class1();
target.Bind(Class2.BarProperty, Observable.Never<string>().StartWith("foo"));
target.Bind(Class2.BarProperty, Observable.Never<BindingValue<string>>().StartWith("foo"));
Assert.Equal("foo", target.GetValue(Class2.BarProperty));
}
@ -403,17 +489,122 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Observable_Is_Unsubscribed_When_Subscription_Disposed()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable<string>();
var source = new TestSubject<BindingValue<string>>("foo");
var target = new Class1();
var subscription = target.Bind(Class1.FooProperty, source);
Assert.Equal(1, source.Subscriptions.Count);
Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe);
Assert.Equal(1, source.SubscriberCount);
subscription.Dispose();
Assert.Equal(1, source.Subscriptions.Count);
Assert.Equal(0, source.Subscriptions[0].Unsubscribe);
Assert.Equal(0, source.SubscriberCount);
}
[Theory]
[InlineData(BindingPriority.LocalValue)]
[InlineData(BindingPriority.Style)]
[InlineData(BindingPriority.Animation)]
public void Observable_Is_Unsubscribed_When_New_Binding_Of_Same_Priority_Is_Added(BindingPriority priority)
{
var source1 = new TestSubject<BindingValue<string>>("foo");
var source2 = new TestSubject<BindingValue<string>>("bar");
var target = new Class1();
target.Bind(Class1.FooProperty, source1, priority);
Assert.Equal(1, source1.SubscriberCount);
target.Bind(Class1.FooProperty, source2, priority);
Assert.Equal(1, source2.SubscriberCount);
Assert.Equal(0, source1.SubscriberCount);
}
[Theory]
[InlineData(BindingPriority.Style)]
public void Observable_Is_Unsubscribed_When_New_Binding_Of_Higher_Priority_Is_Added(BindingPriority priority)
{
var source1 = new TestSubject<BindingValue<string>>("foo");
var source2 = new TestSubject<BindingValue<string>>("bar");
var target = new Class1();
target.Bind(Class1.FooProperty, source1, priority);
Assert.Equal(1, source1.SubscriberCount);
target.Bind(Class1.FooProperty, source2, priority - 1);
Assert.Equal(1, source2.SubscriberCount);
Assert.Equal(0, source1.SubscriberCount);
}
[Theory]
[InlineData(BindingPriority.Style)]
[InlineData(BindingPriority.Animation)]
public void Observable_Is_Unsubscribed_When_New_Value_Of_Same_Priority_Is_Added(BindingPriority priority)
{
var source = new TestSubject<BindingValue<string>>("foo");
var target = new Class1();
target.Bind(Class1.FooProperty, source, priority);
Assert.Equal(1, source.SubscriberCount);
target.SetValue(Class1.FooProperty, "foo", priority);
Assert.Equal(0, source.SubscriberCount);
}
[Theory]
[InlineData(BindingPriority.Style)]
public void Observable_Is_Unsubscribed_When_New_Value_Of_Higher_Priority_Is_Added(BindingPriority priority)
{
var source = new TestSubject<BindingValue<string>>("foo");
var target = new Class1();
target.Bind(Class1.FooProperty, source, priority);
Assert.Equal(1, source.SubscriberCount);
target.SetValue(Class1.FooProperty, "foo", priority - 1);
Assert.Equal(0, source.SubscriberCount);
}
[Theory]
[InlineData(BindingPriority.LocalValue)]
[InlineData(BindingPriority.Style)]
public void Observable_Is_Not_Unsubscribed_When_Animation_Value_Is_Set(BindingPriority priority)
{
var source = new TestSubject<BindingValue<string>>("foo");
var target = new Class1();
target.Bind(Class1.FooProperty, source, priority);
Assert.Equal(1, source.SubscriberCount);
target.SetValue(Class1.FooProperty, "bar", BindingPriority.Animation);
Assert.Equal(1, source.SubscriberCount);
}
[Theory]
[InlineData(BindingPriority.LocalValue)]
[InlineData(BindingPriority.Style)]
public void Observable_Is_Not_Unsubscribed_When_Animation_Binding_Is_Added(BindingPriority priority)
{
var source1 = new TestSubject<BindingValue<string>>("foo");
var source2 = new TestSubject<BindingValue<string>>("bar");
var target = new Class1();
target.Bind(Class1.FooProperty, source1, priority);
Assert.Equal(1, source1.SubscriberCount);
target.Bind(Class1.FooProperty, source2, BindingPriority.Animation);
Assert.Equal(1, source1.SubscriberCount);
Assert.Equal(1, source2.SubscriberCount);
}
[Fact]
public void LocalValue_Binding_Is_Not_Unsubscribed_When_LocalValue_Is_Set()
{
var source = new TestSubject<BindingValue<string>>("foo");
var target = new Class1();
target.Bind(Class1.FooProperty, source);
Assert.Equal(1, source.SubscriberCount);
target.SetValue(Class1.FooProperty, "foo");
Assert.Equal(1, source.SubscriberCount);
}
[Fact]
@ -482,7 +673,7 @@ namespace Avalonia.Base.UnitTests
public void Local_Binding_Overwrites_Local_Value()
{
var target = new Class1();
var binding = new Subject<string>();
var binding = new Subject<BindingValue<string>>();
target.Bind(Class1.FooProperty, binding);
@ -660,6 +851,75 @@ namespace Avalonia.Base.UnitTests
}
}
[Fact]
public void Untyped_LocalValue_Binding_Logs_Invalid_Value_Type()
{
var target = new Class1();
var source = new Subject<object?>();
var called = false;
var expectedMessageTemplate = "Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})";
LogCallback checkLogMessage = (level, area, src, mt, pv) =>
{
if (level == LogEventLevel.Warning &&
area == LogArea.Binding &&
mt == expectedMessageTemplate &&
src == target &&
pv[0].GetType() == typeof(Class1) &&
(AvaloniaProperty)pv[1] == Class1.QuxProperty &&
(Type)pv[2] == typeof(double) &&
(string)pv[3] == "foo" &&
(Type)pv[4] == typeof(string))
{
called = true;
}
};
using (TestLogSink.Start(checkLogMessage))
{
target.Bind(Class1.QuxProperty, source);
source.OnNext(1.2);
source.OnNext("foo");
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
Assert.True(called);
}
}
[Fact]
public void Untyped_Style_Binding_Logs_Invalid_Value_Type()
{
var target = new Class1();
var source = new Subject<object?>();
var called = false;
var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}";
var expectedMessage = "Unable to convert object 'foo' of type 'System.String' to type 'System.Double'.";
LogCallback checkLogMessage = (level, area, src, mt, pv) =>
{
if (level == LogEventLevel.Warning &&
area == LogArea.Binding &&
mt == expectedMessageTemplate &&
src == target &&
pv[0].GetType() == typeof(Class1) &&
(AvaloniaProperty)pv[1] == Class1.QuxProperty &&
(string)pv[2] == expectedMessage)
{
called = true;
}
};
using (TestLogSink.Start(checkLogMessage))
{
target.Bind(Class1.QuxProperty, source, BindingPriority.Style);
source.OnNext(1.2);
source.OnNext("foo");
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
Assert.True(called);
}
}
[Fact]
public async Task Bind_With_Scheduler_Executes_On_Scheduler()
{
@ -726,8 +986,9 @@ namespace Avalonia.Base.UnitTests
public void IsAnimating_On_Property_With_Animation_Value_Returns_True()
{
var target = new Class1();
var source = new BehaviorSubject<BindingValue<string>>("foo");
target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation);
target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
Assert.True(target.IsAnimating(Class1.FooProperty));
}
@ -778,6 +1039,20 @@ namespace Avalonia.Base.UnitTests
Assert.True(target.IsAnimating(Class1.FooProperty));
}
[Fact]
public void TwoWay_Binding_Should_Update_Source()
{
var target = new Class1();
var source = new TestTwoWayBindingViewModel();
target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source });
target.DoubleValue = 123.4;
Assert.True(source.SetterCalled);
Assert.Equal(source.Value, 123.4);
}
[Fact]
public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation()
{
@ -786,7 +1061,7 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source });
Assert.False(source.ValueSetterCalled);
Assert.False(source.SetterCalled);
}
[Fact]
@ -797,7 +1072,7 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source });
Assert.False(source.ValueSetterCalled);
Assert.False(source.SetterCalled);
}
[Fact]
@ -818,69 +1093,111 @@ namespace Avalonia.Base.UnitTests
target.Bind(TextBlock.TextProperty, new Binding("[0]", BindingMode.TwoWay));
}
[Fact]
public void Disposing_Completed_Binding_Does_Not_Throw()
[Theory(Skip = "Will need changes to binding internals in order to pass")]
[InlineData(BindingPriority.LocalValue)]
[InlineData(BindingPriority.StyleTrigger)]
[InlineData(BindingPriority.Style)]
public void TwoWay_Binding_Should_Not_Update_Source_When_Higher_Priority_Value_Set(BindingPriority priority)
{
var target = new Class1();
var source = new Subject<string>();
var subscription = target.Bind(Class1.FooProperty, source);
var source = new TestTwoWayBindingViewModel();
var binding = new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source };
source.OnCompleted();
target.Bind(Class1.DoubleValueProperty, binding, priority);
target.SetValue(Class1.DoubleValueProperty, 123.4, priority - 1);
subscription.Dispose();
// Setter should not be called because the TwoWay binding with LocalValue priority
// should be overridden by the animated value and the binding made inactive.
Assert.False(source.SetterCalled);
}
[Fact]
public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_With_Value()
[Theory(Skip = "Will need changes to binding internals in order to pass")]
[InlineData(BindingPriority.LocalValue)]
[InlineData(BindingPriority.StyleTrigger)]
[InlineData(BindingPriority.Style)]
public void TwoWay_Binding_Should_Not_Update_Source_When_Higher_Priority_Binding_Added(BindingPriority priority)
{
var target = new Class1();
var source = new TestTwoWayBindingViewModel();
var binding1 = new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source };
var binding2 = new BehaviorSubject<double>(123.4);
target.Bind(Class1.DoubleValueProperty, binding1, priority);
target.Bind(Class1.DoubleValueProperty, binding2, priority - 1);
// Setter should not be called because the TwoWay binding with LocalValue priority
// should be overridden by the animated binding and the binding made inactive.
Assert.False(source.SetterCalled);
}
[Fact(Skip = "Will need changes to binding internals in order to pass")]
public void TwoWay_Style_Binding_Should_Not_Update_Source_When_StyleTrigger_Value_Set()
{
var target = new Class1();
var source = new TestTwoWayBindingViewModel() { Value = 1 };
source.ResetSetterCalled();
var source = new TestTwoWayBindingViewModel();
target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source });
target.SetValue(Class1.DoubleValueProperty, 123.4, BindingPriority.Animation);
Assert.False(source.ValueSetterCalled);
// Setter should not be called because the TwoWay binding with Style priority
// should be overridden by the animated value and the binding made inactive.
Assert.False(source.SetterCalled);
}
[Fact]
public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer_With_Value()
[Fact(Skip = "Will need changes to binding internals in order to pass")]
public void TwoWay_Style_Binding_Should_Not_Update_Source_When_Animated_Binding_Added()
{
var target = new Class1();
var source = new TestTwoWayBindingViewModel() { [0] = 1 };
source.ResetSetterCalled();
var source1 = new TestTwoWayBindingViewModel();
var source2 = new BehaviorSubject<double>(123.4);
target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source });
target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source1.Value), BindingMode.TwoWay) { Source = source1 });
target.Bind(Class1.DoubleValueProperty, source2, BindingPriority.Animation);
Assert.False(source.ValueSetterCalled);
// Setter should not be called because the TwoWay binding with Style priority
// should be overridden by the animated binding and the binding made inactive.
Assert.False(source1.SetterCalled);
}
[Fact]
public void Disposing_Completed_Binding_Does_Not_Throw()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
var subscription = target.Bind(Class1.FooProperty, source);
source.OnCompleted();
subscription.Dispose();
}
[Fact]
public void Disposing_a_TwoWay_Binding_Should_Set_Default_Value_On_Binding_Target_But_Not_On_Source()
public void Produces_Correct_Values_And_Base_Values_With_Multiple_Animation_Bindings()
{
var target = new Class3();
var target = new Class1();
var source1 = new BehaviorSubject<BindingValue<double>>(12.2);
var source2 = new BehaviorSubject<BindingValue<double>>(13.3);
// Create a source class which has a Value set to -1 and a Minimum set to -2
var source = new TestTwoWayBindingViewModel() { Value = -1, Minimum = -2 };
target.SetValue(Class1.QuxProperty, 11.1);
target.Bind(Class1.QuxProperty, source1, BindingPriority.Animation);
// Reset the setter counter
source.ResetSetterCalled();
Assert.Equal(12.2, target.GetValue(Class1.QuxProperty));
Assert.Equal(11.1, target.GetBaseValue(Class1.QuxProperty));
// 1. bind the minimum
var disposable_1 = target.Bind(Class3.MinimumProperty, new Binding("Minimum", BindingMode.TwoWay) { Source = source });
// 2. Bind the value
var disposable_2 = target.Bind(Class3.ValueProperty, new Binding("Value", BindingMode.TwoWay) { Source = source });
target.Bind(Class1.QuxProperty, source2, BindingPriority.Animation);
// Dispose the minimum binding
disposable_1.Dispose();
// Dispose the value binding
disposable_2.Dispose();
Assert.Equal(13.3, target.GetValue(Class1.QuxProperty));
Assert.Equal(11.1, target.GetBaseValue(Class1.QuxProperty));
source2.OnCompleted();
// The value setter should be called here as we have disposed minimum fist and the default value of minimum is 0, so this should be changed.
Assert.True(source.ValueSetterCalled);
// The minimum value should not be changed in the source.
Assert.False(source.MinimumSetterCalled);
Assert.Equal(12.2, target.GetValue(Class1.QuxProperty));
Assert.Equal(11.1, target.GetBaseValue(Class1.QuxProperty));
source1.OnCompleted();
Assert.Equal(11.1, target.GetValue(Class1.QuxProperty));
Assert.Equal(11.1, target.GetBaseValue(Class1.QuxProperty));
}
/// <summary>
@ -889,9 +1206,9 @@ namespace Avalonia.Base.UnitTests
/// <typeparam name="T">The type of the observable.</typeparam>
/// <param name="value">The value.</param>
/// <returns>The observable.</returns>
private IObservable<T> Single<T>(T value)
private IObservable<BindingValue<T>> Single<T>(T value)
{
return Observable.Never<T>().StartWith(value);
return Observable.Never<BindingValue<T>>().StartWith(value);
}
private class Class1 : AvaloniaObject
@ -918,56 +1235,6 @@ namespace Avalonia.Base.UnitTests
AvaloniaProperty.Register<Class2, string>("Bar", "bardefault");
}
private class Class3 : AvaloniaObject
{
static Class3()
{
MinimumProperty.Changed.Subscribe(x => OnMinimumChanged(x));
}
private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs<double> e)
{
if (e.Sender is Class3 s)
{
s.SetValue(ValueProperty, MathUtilities.Clamp(s.Value, e.NewValue.Value, double.PositiveInfinity));
}
}
/// <summary>
/// Defines the <see cref="Value"/> property.
/// </summary>
public static readonly StyledProperty<double> ValueProperty =
AvaloniaProperty.Register<Class3, double>(nameof(Value), 0);
/// <summary>
/// Gets or sets the Value property
/// </summary>
public double Value
{
get { return GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
/// <summary>
/// Defines the <see cref="Minimum"/> property.
/// </summary>
public static readonly StyledProperty<double> MinimumProperty =
AvaloniaProperty.Register<Class3, double>(nameof(Minimum), 0);
/// <summary>
/// Gets or sets the minimum property
/// </summary>
public double Minimum
{
get { return GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
}
private class TestOneTimeBinding : IBinding
{
private IObservable<object> _source;
@ -979,8 +1246,8 @@ namespace Avalonia.Base.UnitTests
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null,
AvaloniaProperty? targetProperty,
object? anchor = null,
bool enableDataValidation = false)
{
return InstancedBinding.OneTime(_source);
@ -995,7 +1262,7 @@ namespace Avalonia.Base.UnitTests
private double _value;
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public double Value
{
@ -1008,8 +1275,10 @@ namespace Avalonia.Base.UnitTests
if (SetterInvokedCount < MaxInvokedCount)
{
_value = (int)value;
if (_value > 75) _value = 75;
if (_value < 25) _value = 25;
if (_value > 75)
_value = 75;
if (_value < 25)
_value = 25;
}
else
{
@ -1032,18 +1301,7 @@ namespace Avalonia.Base.UnitTests
set
{
_value = value;
ValueSetterCalled = true;
}
}
private double _minimum;
public double Minimum
{
get => _minimum;
set
{
_minimum = value;
MinimumSetterCalled = true;
SetterCalled = true;
}
}
@ -1053,18 +1311,11 @@ namespace Avalonia.Base.UnitTests
set
{
_value = value;
ValueSetterCalled = true;
SetterCalled = true;
}
}
public bool ValueSetterCalled { get; private set; }
public bool MinimumSetterCalled { get; private set; }
public void ResetSetterCalled()
{
ValueSetterCalled = false;
MinimumSetterCalled = false;
}
public bool SetterCalled { get; private set; }
}
}
}

120
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using Avalonia.Data;
using Xunit;
@ -52,6 +53,64 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(50, target.Foo);
}
[Fact]
public void CoerceValue_Updates_Base_Value()
{
var target = new Class1 { Foo = 99 };
target.SetValue(Class1.FooProperty, 88, BindingPriority.Animation);
Assert.Equal(88, target.Foo);
Assert.Equal(99, target.GetBaseValue(Class1.FooProperty));
target.MaxFoo = 50;
target.CoerceValue(Class1.FooProperty);
Assert.Equal(50, target.Foo);
Assert.Equal(50, target.GetBaseValue(Class1.FooProperty));
}
[Fact]
public void CoerceValue_Raises_PropertyChanged()
{
var target = new Class1 { Foo = 99 };
var raised = 0;
target.PropertyChanged += (s, e) =>
{
Assert.Equal(Class1.FooProperty, e.Property);
Assert.Equal(99, e.OldValue);
Assert.Equal(50, e.NewValue);
Assert.Equal(BindingPriority.LocalValue, e.Priority);
++raised;
};
Assert.Equal(99, target.Foo);
target.MaxFoo = 50;
target.CoerceValue(Class1.FooProperty);
Assert.Equal(50, target.Foo);
Assert.Equal(1, raised);
}
[Fact]
public void CoerceValue_Raises_PropertyChangedCore_For_Base_Value()
{
var target = new Class1 { Foo = 99 };
target.SetValue(Class1.FooProperty, 88, BindingPriority.Animation);
Assert.Equal(88, target.Foo);
Assert.Equal(99, target.GetBaseValue(Class1.FooProperty));
target.MaxFoo = 50;
target.CoreChanges.Clear();
target.CoerceValue(Class1.FooProperty);
Assert.Equal(2, target.CoreChanges.Count);
}
[Fact]
public void Coerced_Value_Can_Be_Restored_If_Limit_Changed()
{
@ -73,7 +132,7 @@ namespace Avalonia.Base.UnitTests
var source1 = new Subject<BindingValue<int>>();
var source2 = new Subject<BindingValue<int>>();
target.Bind(Class1.FooProperty, source1);
target.Bind(Class1.FooProperty, source1, BindingPriority.Style);
source1.OnNext(150);
target.Bind(Class1.FooProperty, source2);
@ -87,6 +146,32 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(150, target.Foo);
}
[Fact]
public void CoerceValue_Updates_Inherited_Value()
{
var parent = new Class1 { Inherited = 99 };
var child = new AvaloniaObject { InheritanceParent = parent };
var raised = 0;
child.InheritanceParent = parent;
child.PropertyChanged += (s, e) =>
{
Assert.Equal(Class1.InheritedProperty, e.Property);
Assert.Equal(99, e.OldValue);
Assert.Equal(50, e.NewValue);
Assert.Equal(BindingPriority.Inherited, e.Priority);
++raised;
};
Assert.Equal(99, child.GetValue(Class1.InheritedProperty));
parent.MaxFoo = 50;
parent.CoerceValue(Class1.InheritedProperty);
Assert.Equal(50, child.GetValue(Class1.InheritedProperty));
Assert.Equal(1, raised);
}
[Fact]
public void Coercion_Can_Be_Overridden()
{
@ -111,18 +196,51 @@ namespace Avalonia.Base.UnitTests
defaultValue: 11,
coerce: CoerceFoo);
public static readonly StyledProperty<int> InheritedProperty =
AvaloniaProperty.RegisterAttached<Class1, Class1, int>(
"Attached",
defaultValue: 11,
inherits: true,
coerce: CoerceFoo);
public int Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
public int Inherited
{
get => GetValue(InheritedProperty);
set => SetValue(InheritedProperty, value);
}
public int MaxFoo { get; set; } = 100;
public List<AvaloniaPropertyChangedEventArgs> CoreChanges { get; } = new();
public static int CoerceFoo(IAvaloniaObject instance, int value)
{
return Math.Min(((Class1)instance).MaxFoo, value);
}
protected override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
{
CoreChanges.Add(Clone(change));
base.OnPropertyChangedCore(change);
}
private static AvaloniaPropertyChangedEventArgs Clone(AvaloniaPropertyChangedEventArgs change)
{
var e = (AvaloniaPropertyChangedEventArgs<int>)change;
return new AvaloniaPropertyChangedEventArgs<int>(
change.Sender,
e.Property,
e.OldValue,
e.NewValue,
change.Priority,
change.IsEffectiveValueChange);
}
}
private class Class2 : AvaloniaObject

31
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs

@ -65,53 +65,42 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void GetBaseValue_LocalValue_Ignores_Default_Value()
public void GetBaseValue_Ignores_Default_Value()
{
var target = new Class3();
target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation);
Assert.False(target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).HasValue);
Assert.False(target.GetBaseValue(Class1.FooProperty).HasValue);
}
[Fact]
public void GetBaseValue_LocalValue_Returns_Local_Value()
public void GetBaseValue_Returns_Local_Value()
{
var target = new Class3();
target.SetValue(Class1.FooProperty, "local");
target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation);
Assert.Equal("local", target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).Value);
Assert.Equal("local", target.GetBaseValue(Class1.FooProperty).Value);
}
[Fact]
public void GetBaseValue_LocalValue_Returns_Style_Value()
public void GetBaseValue_Returns_Style_Value()
{
var target = new Class3();
target.SetValue(Class1.FooProperty, "style", BindingPriority.Style);
target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation);
Assert.Equal("style", target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).Value);
Assert.Equal("style", target.GetBaseValue(Class1.FooProperty).Value);
}
[Fact]
public void GetBaseValue_Style_Ignores_LocalValue_Animated_Value()
public void GetBaseValue_Returns_Style_Value_Set_Via_Untyped_Setters()
{
var target = new Class3();
target.Bind(Class1.FooProperty, new BehaviorSubject<string>("animated"), BindingPriority.Animation);
target.SetValue(Class1.FooProperty, "local");
Assert.False(target.GetBaseValue(Class1.FooProperty, BindingPriority.Style).HasValue);
}
[Fact]
public void GetBaseValue_Style_Returns_Style_Value()
{
var target = new Class3();
target.SetValue(Class1.FooProperty, "local");
target.SetValue(Class1.FooProperty, "style", BindingPriority.Style);
target.Bind(Class1.FooProperty, new BehaviorSubject<string>("animated"), BindingPriority.Animation);
Assert.Equal("style", target.GetBaseValue(Class1.FooProperty, BindingPriority.Style));
target.SetValue(Class1.FooProperty, (object)"style", BindingPriority.Style);
target.SetValue(Class1.FooProperty, (object)"animated", BindingPriority.Animation);
Assert.Equal("style", target.GetBaseValue(Class1.FooProperty).Value);
}
private class Class1 : AvaloniaObject

213
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Avalonia.Data;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -6,7 +7,28 @@ namespace Avalonia.Base.UnitTests
public class AvaloniaObjectTests_Inheritance
{
[Fact]
public void GetValue_Returns_Inherited_Value()
public void GetValue_Returns_Inherited_Value_1()
{
Class1 parent = new Class1();
parent.SetValue(Class1.BazProperty, "changed");
Class2 child = new Class2 { Parent = parent };
Assert.Equal("changed", child.GetValue(Class1.BazProperty));
}
[Fact]
public void GetValue_Returns_Inherited_Value_2()
{
Class1 parent = new Class1();
Class2 child = new Class2 { Parent = parent };
parent.SetValue(Class1.BazProperty, "changed");
Assert.Equal("changed", child.GetValue(Class1.BazProperty));
}
[Fact]
public void ClearValue_Clears_Inherited_Value()
{
Class1 parent = new Class1();
Class2 child = new Class2 { Parent = parent };
@ -14,10 +36,62 @@ namespace Avalonia.Base.UnitTests
parent.SetValue(Class1.BazProperty, "changed");
Assert.Equal("changed", child.GetValue(Class1.BazProperty));
parent.ClearValue(Class1.BazProperty);
Assert.Equal("bazdefault", parent.GetValue(Class1.BazProperty));
Assert.Equal("bazdefault", child.GetValue(Class1.BazProperty));
}
[Fact]
public void ClearValue_On_Parent_Raises_PropertyChanged_On_Child()
{
Class1 parent = new Class1();
Class2 child = new Class2 { Parent = parent };
var raised = 0;
parent.SetValue(Class1.BazProperty, "changed");
child.PropertyChanged += (s, e) =>
{
Assert.Same(child, e.Sender);
Assert.Equal("changed", e.OldValue);
Assert.Equal("bazdefault", e.NewValue);
Assert.Equal(BindingPriority.Inherited, e.Priority);
++raised;
};
parent.ClearValue(Class1.BazProperty);
Assert.Equal(1, raised);
}
[Fact]
public void ClearValue_On_Child_Raises_PropertyChanged_With_Inherited_Parent_Value()
{
Class1 parent = new Class1();
Class2 child = new Class2 { Parent = parent };
var raised = 0;
parent.SetValue(Class1.BazProperty, "parent");
child.SetValue(Class1.BazProperty, "child");
child.PropertyChanged += (s, e) =>
{
Assert.Same(child, e.Sender);
Assert.Equal("child", e.OldValue);
Assert.Equal("parent", e.NewValue);
Assert.Equal(BindingPriority.Inherited, e.Priority);
++raised;
};
child.ClearValue(Class1.BazProperty);
Assert.Equal(1, raised);
}
[Fact]
public void Setting_InheritanceParent_Raises_PropertyChanged_When_Value_Changed_In_Parent()
public void Setting_InheritanceParent_Raises_PropertyChanged_When_Parent_Has_Value_Set()
{
bool raised = false;
@ -29,15 +103,41 @@ namespace Avalonia.Base.UnitTests
raised = s == child &&
e.Property == Class1.BazProperty &&
(string)e.OldValue == "bazdefault" &&
(string)e.NewValue == "changed";
(string)e.NewValue == "changed" &&
e.Priority == BindingPriority.Inherited;
child.Parent = parent;
Assert.True(raised);
Assert.Equal("changed", child.GetValue(Class1.BazProperty));
}
[Fact]
public void Setting_InheritanceParent_Raises_PropertyChanged_When_Parent_And_Grandparent_Has_Value_Set()
{
Class1 grandparent = new Class1();
Class2 parent = new Class2 { Parent = grandparent };
bool raised = false;
grandparent.SetValue(Class1.BazProperty, "changed1");
parent.SetValue(Class1.BazProperty, "changed2");
Class2 child = new Class2();
child.PropertyChanged += (s, e) =>
raised = s == child &&
e.Property == Class1.BazProperty &&
(string)e.OldValue == "bazdefault" &&
(string)e.NewValue == "changed2" &&
e.Priority == BindingPriority.Inherited;
child.Parent = parent;
Assert.True(raised);
Assert.Equal("changed2", child.GetValue(Class1.BazProperty));
}
[Fact]
public void Setting_InheritanceParent_Raises_PropertyChanged_For_Attached_Property_When_Value_Changed_In_Parent()
public void Setting_InheritanceParent_Raises_PropertyChanged_For_Attached_Property_When_Parent_Has_Value_Set()
{
bool raised = false;
@ -54,6 +154,7 @@ namespace Avalonia.Base.UnitTests
child.Parent = parent;
Assert.True(raised);
Assert.Equal("changed", child.GetValue(AttachedOwner.AttachedProperty));
}
[Fact]
@ -71,6 +172,7 @@ namespace Avalonia.Base.UnitTests
child.Parent = parent;
Assert.False(raised);
Assert.Equal("localvalue", child.GetValue(Class1.BazProperty));
}
[Fact]
@ -91,6 +193,7 @@ namespace Avalonia.Base.UnitTests
parent.SetValue(Class1.BazProperty, "changed");
Assert.True(raised);
Assert.Equal("changed", child.GetValue(Class1.BazProperty));
}
[Fact]
@ -111,6 +214,29 @@ namespace Avalonia.Base.UnitTests
parent.SetValue(AttachedOwner.AttachedProperty, "changed");
Assert.True(raised);
Assert.Equal("changed", child.GetValue(AttachedOwner.AttachedProperty));
}
[Fact]
public void Clearing_Value_In_InheritanceParent_Raises_PropertyChanged()
{
bool raised = false;
Class1 parent = new Class1();
parent.SetValue(Class1.BazProperty, "changed");
Class2 child = new Class2 { Parent = parent };
child.PropertyChanged += (s, e) =>
raised = s == child &&
e.Property == Class1.BazProperty &&
(string)e.OldValue == "changed" &&
(string)e.NewValue == "bazdefault";
parent.ClearValue(Class1.BazProperty);
Assert.True(raised);
Assert.Equal("bazdefault", child.GetValue(Class1.BazProperty));
}
[Fact]
@ -128,6 +254,85 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(new[] { parent, child }, result);
}
[Fact]
public void Reparenting_Raises_PropertyChanged_For_Old_And_New_Inherited_Values()
{
var oldParent = new Class1();
oldParent.SetValue(Class1.BazProperty, "oldvalue");
var newParent = new Class1();
newParent.SetValue(Class1.BazProperty, "newvalue");
var child = new Class2 { Parent = oldParent };
var raised = 0;
child.PropertyChanged += (s, e) =>
{
Assert.Equal(child, e.Sender);
Assert.Equal("oldvalue", e.GetOldValue<string>());
Assert.Equal("newvalue", e.GetNewValue<string>());
Assert.Equal(BindingPriority.Inherited, e.Priority);
++raised;
};
child.Parent = newParent;
Assert.Equal(1, raised);
Assert.Equal("newvalue", child.GetValue(Class1.BazProperty));
}
[Fact]
public void Reparenting_Raises_PropertyChanged_On_GrandChild_For_Old_And_New_Inherited_Values()
{
var oldParent = new Class1();
oldParent.SetValue(Class1.BazProperty, "oldvalue");
var newParent = new Class1();
newParent.SetValue(Class1.BazProperty, "newvalue");
var child = new Class2 { Parent = oldParent };
var grandchild = new Class2 { Parent = child };
var raised = 0;
grandchild.PropertyChanged += (s, e) =>
{
Assert.Equal(grandchild, e.Sender);
Assert.Equal("oldvalue", e.GetOldValue<string>());
Assert.Equal("newvalue", e.GetNewValue<string>());
Assert.Equal(BindingPriority.Inherited, e.Priority);
++raised;
};
child.Parent = newParent;
Assert.Equal(1, raised);
Assert.Equal("newvalue", grandchild.GetValue(Class1.BazProperty));
}
[Fact]
public void Reparenting_Retains_Inherited_Property_Set_On_Child()
{
var oldParent = new Class1();
oldParent.SetValue(Class1.BazProperty, "oldvalue");
var newParent = new Class1();
newParent.SetValue(Class1.BazProperty, "newvalue");
var child = new Class2 { Parent = oldParent };
child.SetValue(Class1.BazProperty, "childvalue");
var grandchild = new Class2 { Parent = child };
var raised = 0;
grandchild.PropertyChanged += (s, e) => ++raised;
child.Parent = newParent;
Assert.Equal(0, raised);
Assert.Equal("childvalue", child.GetValue(Class1.BazProperty));
Assert.Equal("childvalue", grandchild.GetValue(Class1.BazProperty));
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =

61
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs

@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "newvalue");
target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Animation);
target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style);
Assert.Equal(2, target.CoreChanges.Count);
@ -49,38 +49,23 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void OnPropertyChangedCore_Is_Called_On_All_Binding_Property_Changes()
public void OnPropertyChangedCore_Is_Called_On_Non_Effective_Property_Binding_Value_Change()
{
var target = new Class1();
var style = new Subject<BindingValue<string>>();
var animation = new Subject<BindingValue<string>>();
var templatedParent = new Subject<BindingValue<string>>();
target.Bind(Class1.FooProperty, style, BindingPriority.Style);
target.Bind(Class1.FooProperty, animation, BindingPriority.Animation);
target.Bind(Class1.FooProperty, templatedParent, BindingPriority.TemplatedParent);
style.OnNext("style1");
templatedParent.OnNext("tp1");
animation.OnNext("a1");
templatedParent.OnNext("tp2");
templatedParent.OnCompleted();
animation.OnNext("a2");
style.OnNext("style2");
style.OnCompleted();
animation.OnCompleted();
var changes = target.CoreChanges.Cast<AvaloniaPropertyChangedEventArgs<string>>();
Assert.Equal(
new[] { true, true, true, false, false, true, false, false, true },
changes.Select(x => x.IsEffectiveValueChange).ToList());
Assert.Equal(
new[] { "style1", "tp1", "a1", "tp2", "$unset", "a2", "style2", "$unset", "foodefault" },
changes.Select(x => x.NewValue.GetValueOrDefault("$unset")).ToList());
Assert.Equal(
new[] { "foodefault", "style1", "tp1", "$unset", "$unset", "a1", "$unset", "$unset", "a2" },
changes.Select(x => x.OldValue.GetValueOrDefault("$unset")).ToList());
var source = new BehaviorSubject<BindingValue<string>>("styled1");
target.Bind(Class1.FooProperty, source, BindingPriority.Style);
target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Animation);
source.OnNext("styled2");
Assert.Equal(3, target.CoreChanges.Count);
var change = (AvaloniaPropertyChangedEventArgs<string>)target.CoreChanges[2];
Assert.Equal("styled2", change.NewValue.Value);
Assert.False(change.OldValue.HasValue);
Assert.Equal(BindingPriority.Style, change.Priority);
Assert.False(change.IsEffectiveValueChange);
}
[Fact]
@ -88,7 +73,7 @@ namespace Avalonia.Base.UnitTests
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "newvalue");
target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Animation);
target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style);
Assert.Equal(1, target.Changes.Count);
@ -124,19 +109,13 @@ namespace Avalonia.Base.UnitTests
private static AvaloniaPropertyChangedEventArgs Clone(AvaloniaPropertyChangedEventArgs change)
{
var e = (AvaloniaPropertyChangedEventArgs<string>)change;
var result = new AvaloniaPropertyChangedEventArgs<string>(
return new AvaloniaPropertyChangedEventArgs<string>(
change.Sender,
e.Property,
e.OldValue,
e.NewValue,
change.Priority);
if (!change.IsEffectiveValueChange)
{
result.MarkNonEffectiveValue();
}
return result;
change.Priority,
change.IsEffectiveValueChange);
}
}
}

28
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs

@ -1,6 +1,7 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Controls;
using Avalonia.Data;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -41,7 +42,7 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void Reverts_To_DefaultValue_If_Binding_Fails_Validation()
public void Reverts_To_DefaultValue_If_LocalValue_Binding_Fails_Validation()
{
var target = new Class1();
var source = new Subject<int>();
@ -52,6 +53,31 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(11, target.GetValue(Class1.FooProperty));
}
[Fact]
public void Reverts_To_DefaultValue_If_Style_Binding_Fails_Validation()
{
var target = new Class1();
var source = new Subject<int>();
target.Bind(Class1.FooProperty, source, BindingPriority.Style);
source.OnNext(150);
Assert.Equal(11, target.GetValue(Class1.FooProperty));
}
[Fact]
public void Reverts_To_Lower_Priority_If_Style_Binding_Fails_Validation()
{
var target = new Class1();
var source = new Subject<int>();
target.SetValue(Class1.FooProperty, 10, BindingPriority.Style);
target.Bind(Class1.FooProperty, source, BindingPriority.StyleTrigger);
source.OnNext(150);
Assert.Equal(10, target.GetValue(Class1.FooProperty));
}
[Fact]
public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings()
{

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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.Utilities;
using Xunit;
@ -149,7 +150,7 @@ namespace Avalonia.Base.UnitTests
internal override IDisposable RouteBind(
AvaloniaObject o,
IObservable<BindingValue<object>> source,
IObservable<object> source,
BindingPriority priority)
{
throw new NotImplementedException();
@ -165,12 +166,7 @@ namespace Avalonia.Base.UnitTests
throw new NotImplementedException();
}
internal override object RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority)
{
throw new NotImplementedException();
}
internal override void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject oldParent)
internal override object RouteGetBaseValue(AvaloniaObject o)
{
throw new NotImplementedException();
}
@ -183,7 +179,7 @@ namespace Avalonia.Base.UnitTests
throw new NotImplementedException();
}
internal override ISetterInstance CreateSetterInstance(IStyleable target, object value)
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{
throw new NotImplementedException();
}

27
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs

@ -3,14 +3,13 @@ using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Microsoft.Reactive.Testing;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Microsoft.Reactive.Testing;
using Xunit;
using System.Threading.Tasks;
using Avalonia.Markup.Parsers;
using Avalonia.Threading;
namespace Avalonia.Base.UnitTests.Data.Core
{
@ -636,7 +635,25 @@ namespace Avalonia.Base.UnitTests.Data.Core
target.Subscribe(x => result.Add(x));
}
[Fact]
public void RootGetter_Is_Reevaluated_On_Subscribe()
{
var data = "foo";
var target = new ExpressionObserver(() => data, new EmptyExpressionNode(), new Subject<Unit>(), null);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
Assert.Equal(new object[] { "foo" }, result);
sub.Dispose();
data = "bar";
target.Subscribe(x => result.Add(x));
Assert.Equal(new object[] { "foo", "bar" }, result);
}
public class MyViewModelBase { public object Name => "Name"; }
public class MyViewModel : MyViewModelBase { public new string Name => "NewName"; }

314
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@ -1,314 +0,0 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class PriorityValueTests
{
private static readonly AvaloniaObject Owner = new AvaloniaObject();
private static readonly ValueStore ValueStore = new ValueStore(Owner);
private static readonly StyledProperty<string> TestProperty = new StyledProperty<string>(
"Test",
typeof(PriorityValueTests),
new StyledPropertyMetadata<string>());
[Fact]
public void Constructor_Should_Set_Value_Based_On_Initial_Entry()
{
var target = new PriorityValue<string>(
Owner,
TestProperty,
ValueStore,
new ConstantValueEntry<string>(
TestProperty,
"1",
BindingPriority.StyleTrigger,
new(ValueStore)));
Assert.Equal("1", target.GetValue().Value);
Assert.Equal(BindingPriority.StyleTrigger, target.Priority);
}
[Fact]
public void GetValue_Should_Respect_MaxPriority()
{
var target = new PriorityValue<string>(
Owner,
TestProperty,
ValueStore);
target.SetValue("animation", BindingPriority.Animation);
target.SetValue("local", BindingPriority.LocalValue);
target.SetValue("styletrigger", BindingPriority.StyleTrigger);
target.SetValue("style", BindingPriority.Style);
Assert.Equal("animation", target.GetValue(BindingPriority.Animation));
Assert.Equal("local", target.GetValue(BindingPriority.LocalValue));
Assert.Equal("styletrigger", target.GetValue(BindingPriority.StyleTrigger));
Assert.Equal("style", target.GetValue(BindingPriority.TemplatedParent));
Assert.Equal("style", target.GetValue(BindingPriority.Style));
}
[Fact]
public void SetValue_LocalValue_Should_Not_Add_Entries()
{
var target = new PriorityValue<string>(
Owner,
TestProperty,
ValueStore);
target.SetValue("1", BindingPriority.LocalValue);
target.SetValue("2", BindingPriority.LocalValue);
Assert.Empty(target.Entries);
}
[Fact]
public void SetValue_Non_LocalValue_Should_Add_Entries()
{
var target = new PriorityValue<string>(
Owner,
TestProperty,
ValueStore);
target.SetValue("1", BindingPriority.Style);
target.SetValue("2", BindingPriority.Animation);
var result = target.Entries
.OfType<ConstantValueEntry<string>>()
.Select(x => x.GetValue().Value)
.ToList();
Assert.Equal(new[] { "1", "2" }, result);
}
[Fact]
public void Priority_Should_Be_Set()
{
var target = new PriorityValue<string>(
Owner,
TestProperty,
ValueStore);
Assert.Equal(BindingPriority.Unset, target.Priority);
target.SetValue("style", BindingPriority.Style);
Assert.Equal(BindingPriority.Style, target.Priority);
target.SetValue("local", BindingPriority.LocalValue);
Assert.Equal(BindingPriority.LocalValue, target.Priority);
target.SetValue("animation", BindingPriority.Animation);
Assert.Equal(BindingPriority.Animation, target.Priority);
target.SetValue("local2", BindingPriority.LocalValue);
Assert.Equal(BindingPriority.Animation, target.Priority);
}
[Fact]
public void Binding_With_Same_Priority_Should_Be_Appended()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
var source2 = new Source("2");
target.AddBinding(source1, BindingPriority.LocalValue);
target.AddBinding(source2, BindingPriority.LocalValue);
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
Assert.Equal(new[] { "1", "2" }, result);
}
[Fact]
public void Binding_With_Higher_Priority_Should_Be_Appended()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
var source2 = new Source("2");
target.AddBinding(source1, BindingPriority.LocalValue);
target.AddBinding(source2, BindingPriority.Animation);
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
Assert.Equal(new[] { "1", "2" }, result);
}
[Fact]
public void Binding_With_Lower_Priority_Should_Be_Prepended()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
var source2 = new Source("2");
target.AddBinding(source1, BindingPriority.LocalValue);
target.AddBinding(source2, BindingPriority.Style);
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
Assert.Equal(new[] { "2", "1" }, result);
}
[Fact]
public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
var source2 = new Source("2");
var source3 = new Source("3");
target.AddBinding(source1, BindingPriority.LocalValue);
target.AddBinding(source2, BindingPriority.Style);
target.AddBinding(source3, BindingPriority.Style);
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
Assert.Equal(new[] { "2", "3", "1" }, result);
}
[Fact]
public void Competed_Binding_Should_Be_Removed()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
var source2 = new Source("2");
var source3 = new Source("3");
target.AddBinding(source1, BindingPriority.LocalValue).Start();
target.AddBinding(source2, BindingPriority.Style).Start();
target.AddBinding(source3, BindingPriority.Style).Start();
source3.OnCompleted();
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
Assert.Equal(new[] { "2", "1" }, result);
}
[Fact]
public void Value_Should_Come_From_Last_Entry()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
var source2 = new Source("2");
var source3 = new Source("3");
target.AddBinding(source1, BindingPriority.LocalValue).Start();
target.AddBinding(source2, BindingPriority.Style).Start();
target.AddBinding(source3, BindingPriority.Style).Start();
Assert.Equal("1", target.GetValue().Value);
}
[Fact]
public void LocalValue_Should_Override_LocalValue_Binding()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
target.AddBinding(source1, BindingPriority.LocalValue).Start();
target.SetValue("2", BindingPriority.LocalValue);
Assert.Equal("2", target.GetValue().Value);
}
[Fact]
public void LocalValue_Should_Override_Style_Binding()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
target.AddBinding(source1, BindingPriority.Style).Start();
target.SetValue("2", BindingPriority.LocalValue);
Assert.Equal("2", target.GetValue().Value);
}
[Fact]
public void LocalValue_Should_Not_Override_Animation_Binding()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
target.AddBinding(source1, BindingPriority.Animation).Start();
target.SetValue("2", BindingPriority.LocalValue);
Assert.Equal("1", target.GetValue().Value);
}
[Fact]
public void NonAnimated_Value_Should_Be_Correct_1()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
var source2 = new Source("2");
var source3 = new Source("3");
target.AddBinding(source1, BindingPriority.LocalValue).Start();
target.AddBinding(source2, BindingPriority.Style).Start();
target.AddBinding(source3, BindingPriority.Animation).Start();
Assert.Equal("3", target.GetValue().Value);
Assert.Equal("1", target.GetValue(BindingPriority.LocalValue).Value);
}
[Fact]
public void NonAnimated_Value_Should_Be_Correct_2()
{
var target = new PriorityValue<string>(Owner, TestProperty, ValueStore);
var source1 = new Source("1");
var source2 = new Source("2");
var source3 = new Source("3");
target.AddBinding(source1, BindingPriority.Animation).Start();
target.AddBinding(source2, BindingPriority.Style).Start();
target.AddBinding(source3, BindingPriority.Style).Start();
Assert.Equal("1", target.GetValue().Value);
Assert.Equal("3", target.GetValue(BindingPriority.LocalValue).Value);
}
private class Source : IObservable<BindingValue<string>>
{
private IObserver<BindingValue<string>> _observer;
public Source(string id) => Id = id;
public string Id { get; }
public IDisposable Subscribe(IObserver<BindingValue<string>> observer)
{
_observer = observer;
observer.OnNext(Id);
return Disposable.Empty;
}
public void OnCompleted() => _observer.OnCompleted();
}
}
}

142
tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs

@ -0,0 +1,142 @@
using System.Collections.Generic;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Microsoft.Reactive.Testing;
using Xunit;
using static Microsoft.Reactive.Testing.ReactiveTest;
#nullable enable
namespace Avalonia.Base.UnitTests.PropertyStore
{
public class ValueStoreTests_Frames
{
[Fact]
public void Adding_Frame_Raises_PropertyChanged()
{
var target = new Class1();
var subject = new BehaviorSubject<string>("bar");
var result = new List<PropertyChange>();
var style = new Style
{
Setters =
{
new Setter(Class1.FooProperty, "foo"),
new Setter(Class1.BarProperty, subject.ToBinding()),
}
};
target.PropertyChanged += (s, e) =>
{
result.Add(new(e.Property, e.OldValue, e.NewValue));
};
var frame = InstanceStyle(style, target);
target.GetValueStore().AddFrame(frame);
Assert.Equal(new PropertyChange[]
{
new(Class1.FooProperty, "foodefault", "foo"),
new(Class1.BarProperty, "bardefault", "bar"),
}, result);
}
[Fact]
public void Removing_Frame_Raises_PropertyChanged()
{
var target = new Class1();
var subject = new BehaviorSubject<string>("bar");
var result = new List<PropertyChange>();
var style = new Style
{
Setters =
{
new Setter(Class1.FooProperty, "foo"),
new Setter(Class1.BarProperty, subject.ToBinding()),
}
};
var frame = InstanceStyle(style, target);
target.GetValueStore().AddFrame(frame);
target.PropertyChanged += (s, e) =>
{
result.Add(new(e.Property, e.OldValue, e.NewValue));
};
target.GetValueStore().RemoveFrame(frame);
Assert.Equal(new PropertyChange[]
{
new(Class1.BarProperty, "bar", "bardefault"),
new(Class1.FooProperty, "foo", "foodefault"),
}, result);
}
[Fact]
public void Removing_Frame_Unsubscribes_Binding()
{
var target = new Class1();
var scheduler = new TestScheduler();
var obs = scheduler.CreateColdObservable(OnNext(0, "bar"));
var style = new Style
{
Setters =
{
new Setter(Class1.FooProperty, "foo"),
new Setter(Class1.BarProperty, obs.ToBinding()),
}
};
var frame = InstanceStyle(style, target);
target.GetValueStore().AddFrame(frame);
target.GetValueStore().RemoveFrame(frame);
Assert.Single(obs.Subscriptions);
Assert.Equal(0, obs.Subscriptions[0].Subscribe);
Assert.NotEqual(Subscription.Infinite, obs.Subscriptions[0].Unsubscribe);
}
[Fact]
public void Completing_Binding_Removes_ImmediateValueFrame()
{
var target = new Class1();
var source = new BehaviorSubject<BindingValue<string>>("foo");
target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
var valueStore = target.GetValueStore();
Assert.Equal(1, valueStore.Frames.Count);
Assert.IsType<ImmediateValueFrame>(valueStore.Frames[0]);
source.OnCompleted();
Assert.Equal(0, valueStore.Frames.Count);
}
private static StyleInstance InstanceStyle(Style style, StyledElement target)
{
var result = new StyleInstance(style, null);
foreach (var setter in style.Setters)
result.Add(setter.Instance(result, target));
return result;
}
private class Class1 : StyledElement
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", "foodefault");
public static readonly StyledProperty<string> BarProperty =
AvaloniaProperty.Register<Class1, string>("Bar", "bardefault", true);
}
private record PropertyChange(
AvaloniaProperty Property,
object? OldValue,
object? NewValue);
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save