Browse Source

Initial refactor of AvaloniaObject value store.

Most (but not all) tests passing, all features mostly implemented exception coercion.
refactor/style-priorities
Steven Kirk 4 years ago
parent
commit
71785b73d8
  1. 2
      src/Avalonia.Base/Animation/Animatable.cs
  2. 2
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  3. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  4. 440
      src/Avalonia.Base/AvaloniaObject.cs
  5. 18
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  6. 24
      src/Avalonia.Base/AvaloniaProperty.cs
  7. 24
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  8. 28
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  9. 33
      src/Avalonia.Base/CollectionPolyfills.cs
  10. 5
      src/Avalonia.Base/Data/BindingPriority.cs
  11. 29
      src/Avalonia.Base/Data/BindingValue.cs
  12. 54
      src/Avalonia.Base/DirectPropertyBase.cs
  13. 7
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  14. 191
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  15. 169
      src/Avalonia.Base/PropertyStore/BindingEntry`1.cs
  16. 82
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  17. 25
      src/Avalonia.Base/PropertyStore/DictionaryPool.cs
  18. 117
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  19. 220
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  20. 8
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  21. 18
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  22. 28
      src/Avalonia.Base/PropertyStore/IValue.cs
  23. 42
      src/Avalonia.Base/PropertyStore/IValueEntry.cs
  24. 33
      src/Avalonia.Base/PropertyStore/IValueEntry`1.cs
  25. 56
      src/Avalonia.Base/PropertyStore/IValueFrame.cs
  26. 44
      src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
  27. 60
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  28. 34
      src/Avalonia.Base/PropertyStore/InheritanceFrame.cs
  29. 59
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  30. 41
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  31. 61
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  32. 61
      src/Avalonia.Base/PropertyStore/LoggingUtils.cs
  33. 326
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  34. 163
      src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs
  35. 37
      src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs
  36. 54
      src/Avalonia.Base/PropertyStore/ValueFrameBase.cs
  37. 45
      src/Avalonia.Base/PropertyStore/ValueOwner.cs
  38. 948
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  39. 130
      src/Avalonia.Base/StyledElement.cs
  40. 84
      src/Avalonia.Base/StyledPropertyBase.cs
  41. 17
      src/Avalonia.Base/Styling/Activators/AndActivator.cs
  42. 5
      src/Avalonia.Base/Styling/Activators/IStyleActivator.cs
  43. 1
      src/Avalonia.Base/Styling/Activators/NotActivator.cs
  44. 8
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  45. 17
      src/Avalonia.Base/Styling/Activators/OrActivator.cs
  46. 11
      src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs
  47. 4
      src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs
  48. 8
      src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
  49. 1
      src/Avalonia.Base/Styling/ControlTheme.cs
  50. 6
      src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs
  51. 12
      src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs
  52. 6
      src/Avalonia.Base/Styling/ISetter.cs
  53. 34
      src/Avalonia.Base/Styling/ISetterInstance.cs
  54. 19
      src/Avalonia.Base/Styling/IStyleInstance.cs
  55. 20
      src/Avalonia.Base/Styling/IStyleable.cs
  56. 197
      src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs
  57. 121
      src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
  58. 80
      src/Avalonia.Base/Styling/Setter.cs
  59. 26
      src/Avalonia.Base/Styling/Style.cs
  60. 22
      src/Avalonia.Base/Styling/StyleBase.cs
  61. 143
      src/Avalonia.Base/Styling/StyleInstance.cs
  62. 9
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  63. 507
      src/Avalonia.Base/ValueStore.cs
  64. 33
      src/Avalonia.Base/Visual.cs
  65. 10
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  66. 22
      tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
  67. 695
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  68. 372
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  69. 31
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs
  70. 120
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs
  71. 51
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs
  72. 28
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
  73. 16
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  74. 314
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  75. 126
      tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs
  76. 319
      tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
  77. 92
      tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
  78. 67
      tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs
  79. 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  80. 147
      tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs
  81. 7
      tests/Avalonia.Benchmarks/Styling/Style_Apply.cs
  82. 12
      tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs
  83. 21
      tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs
  84. 2
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.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)!;

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -34,6 +34,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.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />

440
src/Avalonia.Base/AvaloniaObject.cs

@ -5,7 +5,6 @@ using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading;
namespace Avalonia
@ -23,7 +22,7 @@ namespace Avalonia
private PropertyChangedEventHandler? _inpcChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChanged;
private List<AvaloniaObject>? _inheritanceChildren;
private ValueStore? _values;
private ValueStore _values;
private bool _batchUpdate;
/// <summary>
@ -32,6 +31,7 @@ namespace Avalonia
public AvaloniaObject()
{
VerifyAccess();
_values = new ValueStore(this);
}
/// <summary>
@ -59,7 +59,7 @@ namespace Avalonia
/// <value>
/// The inheritance parent.
/// </value>
protected AvaloniaObject? InheritanceParent
protected internal AvaloniaObject? InheritanceParent
{
get
{
@ -77,23 +77,8 @@ namespace Avalonia
_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(oldParent, value);
}
}
}
@ -118,24 +103,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 +120,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>
@ -232,12 +208,7 @@ namespace Avalonia
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
public object? GetValue(AvaloniaProperty property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
return property.RouteGetValue(this);
}
public object? GetValue(AvaloniaProperty property) => property.RouteGetValue(this);
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
@ -247,10 +218,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 +239,10 @@ 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));
VerifyAccess();
if (_values is object &&
_values.TryGetValue(property, maxPriority, out var value))
{
return value;
}
return default;
_ = property ?? throw new ArgumentNullException(nameof(property));
return _values.GetBaseValue(property);
}
/// <summary>
@ -346,26 +308,19 @@ namespace Avalonia
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
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;
@ -389,6 +344,7 @@ namespace Avalonia
/// <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>
@ -398,12 +354,51 @@ 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();
return property.RouteBind(this, source.ToBindingValue(), 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();
return _values.AddBinding(property, source, priority);
}
/// <summary>
@ -425,7 +420,7 @@ namespace Avalonia
source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess();
return Values.AddBinding(property, source, priority);
return _values.AddBinding(property, source, priority);
}
/// <summary>
@ -469,29 +464,8 @@ namespace Avalonia
/// <param name="property">The property.</param>
public void CoerceValue(AvaloniaProperty property)
{
_values?.CoerceValue(property);
}
public void BeginBatchUpdate()
{
if (_batchUpdate)
{
throw new InvalidOperationException("Batch update already in progress.");
}
_batchUpdate = true;
_values?.BeginBatchUpdate();
}
public void EndBatchUpdate()
{
if (!_batchUpdate)
{
throw new InvalidOperationException("No batch update in progress.");
}
_batchUpdate = false;
_values?.EndBatchUpdate();
throw new NotImplementedException();
////_values?.CoerceValue(property);
}
/// <inheritdoc/>
@ -507,98 +481,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 +514,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 +567,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>
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 +590,43 @@ 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>(
this,
property,
oldValue,
newValue,
priority));
if (isEffectiveValue)
property.Notifying?.Invoke(this, true);
try
{
var e = new AvaloniaPropertyChangedEventArgs<T>(
this,
property,
oldValue,
newValue,
priority,
isEffectiveValue);
OnPropertyChangedCore(e);
if (isEffectiveValue)
{
property.NotifyChanged(e);
_propertyChanged?.Invoke(this, e);
_inpcChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name));
}
}
finally
{
if (isEffectiveValue)
property.Notifying?.Invoke(this, false);
}
}
/// <summary>
@ -718,94 +651,10 @@ 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>
@ -839,7 +688,7 @@ namespace Avalonia
throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
}
LogIfError(property, value);
LoggingUtils.LogIfNecessary(this, property, value);
switch (value.Type)
{
@ -877,29 +726,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>

18
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -362,10 +362,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 +371,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 +386,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 +395,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 +404,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.")
};

24
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,11 +502,19 @@ 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>
/// Routes an untyped Bind call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
/// <param name="source">The binding source.</param>
/// <param name="priority">The priority.</param>
internal abstract IDisposable RouteBind(
AvaloniaObject o,
IObservable<BindingValue<object?>> source,
BindingPriority priority);
/// <summary>
/// Overrides the metadata for the property on the specified type.

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);

33
src/Avalonia.Base/CollectionPolyfills.cs

@ -0,0 +1,33 @@
#if !NET6_0_OR_GREATER
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia
{
internal static class CollectionPolyfills
{
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

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.

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Utilities;
@ -245,6 +246,34 @@ namespace Avalonia.Data
};
}
public static bool operator !=(BindingValue<T> x, Optional<T> y)
{
if (x.HasValue != y.HasValue)
return true;
return !EqualityComparer<T>.Default.Equals(x.Value, y.Value);
}
public static bool operator ==(BindingValue<T> x, Optional<T> y)
{
if (x.HasValue != y.HasValue)
return false;
return EqualityComparer<T>.Default.Equals(x.Value, y.Value);
}
public static bool operator !=(Optional<T> x, BindingValue<T> y)
{
if (x.HasValue != y.HasValue)
return true;
return !EqualityComparer<T>.Default.Equals(x.Value, y.Value);
}
public static bool operator ==(Optional<T> x, BindingValue<T> y)
{
if (x.HasValue != y.HasValue)
return false;
return EqualityComparer<T>.Default.Equals(x.Value, y.Value);
}
/// <summary>
/// Creates a binding value from an instance of the underlying value type.
/// </summary>

54
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Styling;
@ -120,6 +121,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)
{
@ -132,7 +138,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);
}
@ -161,6 +167,22 @@ namespace Avalonia
return null;
}
/// <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<object?> source,
BindingPriority priority)
{
// TODO: this requires a double adapter, we should make AvaloniaObject
// accept an `IObservable<object?>` for direct properties directly.
return RouteBind(o, source.ToBindingValue(), priority);
}
/// <inheritdoc/>
internal override IDisposable RouteBind(
AvaloniaObject o,
@ -170,35 +192,5 @@ namespace Avalonia
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!);
}
}
}
}

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

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

@ -1,154 +1,137 @@
using System;
using System.Diagnostics;
using System.Reactive.Disposables;
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
internal class BindingEntry : IValueEntry,
IObserver<object?>,
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 readonly ValueFrameBase _frame;
private readonly IObservable<object?> _source;
private IDisposable? _subscription;
private bool _isSubscribed;
private bool _batchUpdate;
private Optional<T> _value;
private bool _hasValue;
private object? _value;
public BindingEntry(
AvaloniaObject owner,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority,
ValueOwner<T> sink)
ValueFrameBase frame,
AvaloniaProperty property,
IObservable<object?> source)
{
_owner = owner;
_frame = frame;
_source = source;
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()
public bool HasValue
{
_batchUpdate = false;
if (_sink.IsValueStore)
Start();
get
{
StartIfNecessary();
return _hasValue;
}
}
public Optional<T> GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}
public AvaloniaProperty Property { get; }
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
OnCompleted();
Unsubscribe();
BindingCompleted();
}
public void OnCompleted()
public object? GetValue()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_isSubscribed = false;
_sink.Completed(Property, this, oldValue);
StartIfNecessary();
if (!_hasValue)
throw new AvaloniaInternalException("The binding entry has no value.");
return _value!;
}
public void OnError(Exception error)
public bool TryGetValue(out object? value)
{
throw new NotImplementedException("BindingEntry.OnError is not implemented", error);
StartIfNecessary();
value = _value;
return _hasValue;
}
public void OnNext(BindingValue<T> value)
public void Start()
{
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;
Debug.Assert(_subscription is null);
Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue));
}
// Subscription won't be set until Subscribe completes, but in the meantime we
// need to signal that we've started as Subscribe may cause a value to be produced.
_subscription = Disposable.Empty;
_subscription = _source.Subscribe(this);
}
public void Start() => Start(false);
public void OnCompleted() => BindingCompleted();
public void OnError(Exception error) => BindingCompleted();
public void OnNext(object? value) => SetValue(value);
public void Start(bool ignoreBatchUpdate)
public virtual void Unsubscribe()
{
// 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);
}
_subscription?.Dispose();
_subscription = null;
}
public void Reparent(PriorityValue<T> parent) => _sink = new(parent);
public void RaiseValueChanged(
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
private void ClearValue()
{
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
if (_hasValue)
{
_hasValue = false;
_value = default;
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority);
}
}
private void UpdateValue(BindingValue<T> value)
private void SetValue(object? value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
if (_frame.Owner is null)
return;
if (value is BindingNotification n)
{
value = Property.GetDefaultValue(_owner.GetType());
value = n.Value;
}
if (value.Type == BindingValueType.DoNothing)
if (value == AvaloniaProperty.UnsetValue)
{
return;
ClearValue();
}
var old = _value;
if (value.Type != BindingValueType.DataValidationError)
else if (value == BindingOperations.DoNothing)
{
// Do nothing!
}
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue))
{
if (!_hasValue || !Equals(_value, typedValue))
{
_value = typedValue;
_hasValue = true;
_frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, typedValue);
}
}
else
{
_value = value.ToOptional();
ClearValue();
LoggingUtils.LogInvalidValue(_frame.Owner.Owner, Property, Property.PropertyType, value);
}
}
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(_owner, Property, old, value, Priority));
private void BindingCompleted()
{
_subscription = null;
_frame.OnBindingCompleted(this);
}
private void StartIfNecessary()
{
if (_subscription is null)
Start();
}
}
}

169
src/Avalonia.Base/PropertyStore/BindingEntry`1.cs

@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal class BindingEntry<T> : IValueEntry<T>,
IObserver<T>,
IObserver<BindingValue<T>>,
IDisposable
{
private readonly ValueFrameBase _frame;
private readonly object _source;
private IDisposable? _subscription;
private bool _hasValue;
private T? _value;
public BindingEntry(
ValueFrameBase frame,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
_frame = frame;
_source = source;
Property = property;
}
public BindingEntry(
ValueFrameBase frame,
StyledPropertyBase<T> property,
IObservable<T> source)
{
_frame = frame;
_source = source;
Property = property;
}
public bool HasValue
{
get
{
StartIfNecessary();
return _hasValue;
}
}
public StyledPropertyBase<T> Property { get; }
AvaloniaProperty IValueEntry.Property => Property;
public void Dispose()
{
Unsubscribe();
BindingCompleted();
}
public T GetValue()
{
StartIfNecessary();
if (!_hasValue)
throw new AvaloniaInternalException("The binding entry has no value.");
return _value!;
}
public void Start()
{
Debug.Assert(_subscription is null);
// Subscription won't be set until Subscribe completes, but in the meantime we
// need to signal that we've started as Subscribe may cause a value to be produced.
_subscription = Disposable.Empty;
if (_source is IObservable<BindingValue<T>> bv)
_subscription = bv.Subscribe(this);
else if (_source is IObservable<T> b)
_subscription = b.Subscribe(this);
else
throw new AvaloniaInternalException("Unexpected binding source.");
}
public bool TryGetValue([MaybeNullWhen(false)] out T value)
{
StartIfNecessary();
value = _value;
return _hasValue;
}
public void OnCompleted() => BindingCompleted();
public void OnError(Exception error) => BindingCompleted();
public void OnNext(T value) => SetValue(value);
public void OnNext(BindingValue<T> value)
{
if (_frame.Owner is not null)
LoggingUtils.LogIfNecessary(_frame.Owner.Owner, Property, value);
if (value.HasValue)
SetValue(value.Value);
else
ClearValue();
}
public void Unsubscribe()
{
_subscription?.Dispose();
_subscription = null;
}
object? IValueEntry.GetValue()
{
StartIfNecessary();
if (!_hasValue)
throw new AvaloniaInternalException("The BindingEntry<T> has no value.");
return _value!;
}
bool IValueEntry.TryGetValue(out object? value)
{
StartIfNecessary();
value = _value;
return _hasValue;
}
private void ClearValue()
{
if (_hasValue)
{
_hasValue = false;
_value = default;
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority);
}
}
private void SetValue(T value)
{
if (_frame.Owner is null)
return;
if (Property.ValidateValue?.Invoke(value) != false)
{
if (!_hasValue || !EqualityComparer<T>.Default.Equals(_value, value))
{
_value = value;
_hasValue = true;
_frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, value);
}
}
else
{
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority);
}
}
private void BindingCompleted()
{
_subscription = null;
_frame.OnBindingCompleted(this);
}
private void StartIfNecessary()
{
if (_subscription is null)
Start();
}
}
}

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

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

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

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

@ -0,0 +1,117 @@
using System;
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
{
/// <summary>
/// Gets the current effective value as a boxed value.
/// </summary>
public object? Value => GetBoxedValue();
/// <summary>
/// Gets the current effective base value as a boxed value, or
/// <see cref="AvaloniaProperty.UnsetValue"/> if not set.
/// </summary>
public object? BaseValue => GetBoxedBaseValue();
/// <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>
/// Sets the value and 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>
/// <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,
AvaloniaProperty property,
object? value,
BindingPriority priority);
/// <summary>
/// Sets the value and 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>
/// <param name="value">The new value of the property.</param>
/// <param name="priority">The priority of the new value.</param>
/// <param name="baseValue">The new base value of the property.</param>
/// <param name="basePriority">The priority of the new base value.</param>
public abstract void SetAndRaise(
ValueStore owner,
AvaloniaProperty property,
object? value,
BindingPriority priority,
object? baseValue,
BindingPriority basePriority);
/// <summary>
/// Sets the value, raising <see cref="AvaloniaObject.PropertyChanged"/>
/// where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="entry">The value entry with the new value of the property.</param>
/// <param name="priority">The priority of the new value.</param>
/// <remarks>
/// This method does not set the base value.
/// </remarks>
public abstract void SetAndRaise(
ValueStore owner,
IValueEntry entry,
BindingPriority priority);
/// <summary>
/// Set the value priority, but leaves the value unchanged.
/// </summary>
public void SetPriority(BindingPriority priority) => Priority = BindingPriority.Unset;
/// <summary>
/// Set the base value priority, but leaves the base value unchanged.
/// </summary>
public void SetBasePriority(BindingPriority priority) => BasePriority = BindingPriority.Unset;
/// <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>
/// 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 abstract object? GetBoxedBaseValue();
}
}

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

@ -0,0 +1,220 @@
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 T? _baseValue;
public EffectiveValue(T value, BindingPriority priority)
{
Value = value;
Priority = priority;
if (priority >= BindingPriority.LocalValue && priority < BindingPriority.Inherited)
{
_baseValue = value;
BasePriority = priority;
}
else
{
_baseValue = default;
BasePriority = BindingPriority.Unset;
}
}
/// <summary>
/// Gets the current effective value.
/// </summary>
public new T Value { get; private set; }
public override void SetAndRaise(
ValueStore owner,
AvaloniaProperty property,
object? value,
BindingPriority priority)
{
// `value` should already have been converted to the correct type and
// validated by this point.
SetAndRaise(owner, (StyledPropertyBase<T>)property, (T)value!, priority);
}
public override void SetAndRaise(
ValueStore owner,
AvaloniaProperty property,
object? value,
BindingPriority priority,
object? baseValue,
BindingPriority basePriority)
{
SetAndRaise(owner, (StyledPropertyBase<T>)property, (T)value!, priority, (T)baseValue!, basePriority);
}
public override void SetAndRaise(
ValueStore owner,
IValueEntry entry,
BindingPriority priority)
{
var value = entry is IValueEntry<T> typed ? typed.GetValue() : (T)entry.GetValue()!;
SetAndRaise(owner, (StyledPropertyBase<T>)entry.Property, value, priority);
}
/// <summary>
/// Sets the value and base value, raising <see cref="AvaloniaObject.PropertyChanged"/>
/// where necessary.
/// </summary>
/// <param name="owner">The object on which to raise events.</param>
/// <param name="property">The property being changed.</param>
/// <param name="value">The new value of the property.</param>
/// <param name="priority">The priority of the new value.</param>
public void SetAndRaise(
ValueStore owner,
StyledPropertyBase<T> property,
T value,
BindingPriority priority)
{
Debug.Assert(priority < BindingPriority.Inherited);
var oldValue = Value;
var valueChanged = false;
var baseValueChanged = false;
if (priority <= Priority)
{
valueChanged = !EqualityComparer<T>.Default.Equals(Value, value);
Value = value;
Priority = priority;
}
if (priority <= BasePriority && priority >= BindingPriority.LocalValue)
{
baseValueChanged = !EqualityComparer<T>.Default.Equals(_baseValue, value);
_baseValue = value;
BasePriority = priority;
}
if (valueChanged)
{
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);
}
}
/// <summary>
/// Sets the value and base value, raising <see cref="AvaloniaObject.PropertyChanged"/>
/// where necessary.
/// </summary>
/// <param name="owner">The object on which to raise events.</param>
/// <param name="property">The property being changed.</param>
/// <param name="value">The new value of the property.</param>
/// <param name="priority">The priority of the new value.</param>
/// <param name="baseValue">The new base value of the property.</param>
/// <param name="basePriority">The priority of the new base value.</param>
public void SetAndRaise(
ValueStore owner,
StyledPropertyBase<T> property,
T value,
BindingPriority priority,
T baseValue,
BindingPriority basePriority)
{
Debug.Assert(priority < BindingPriority.Inherited);
Debug.Assert(basePriority > BindingPriority.Animation);
var oldValue = Value;
var valueChanged = false;
var baseValueChanged = false;
if (!EqualityComparer<T>.Default.Equals(Value, value))
{
Value = value;
valueChanged = true;
}
if (BasePriority == BindingPriority.Unset ||
!EqualityComparer<T>.Default.Equals(_baseValue, baseValue))
{
_baseValue = value;
baseValueChanged = true;
}
Priority = priority;
BasePriority = basePriority;
if (valueChanged)
{
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);
}
}
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 : p.GetDefaultValue(owner.GetType());
var n = newValue is not null ? ((EffectiveValue<T>)newValue).Value : p.GetDefaultValue(owner.GetType());
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 DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
{
DisposeAndRaiseUnset(owner, (StyledPropertyBase<T>)property);
}
public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase<T> property)
{
var defaultValue = property.GetDefaultValue(owner.GetType());
if (!EqualityComparer<T>.Default.Equals(defaultValue, Value))
{
owner.Owner.RaisePropertyChanged(property, Value, defaultValue, BindingPriority.Unset, true);
if (property.Inherits)
owner.OnInheritedEffectiveValueDisposed(property, Value);
}
}
protected override object? GetBoxedValue() => Value;
protected override object? GetBoxedBaseValue()
{
return BasePriority != BindingPriority.Unset ? _baseValue : AvaloniaProperty.UnsetValue;
}
}
}

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

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

@ -0,0 +1,42 @@
using System;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped value entry in an <see cref="IValueFrame"/>.
/// </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>
/// Tries to get the value associated with the entry.
/// </summary>
/// <param name="value">
/// When this method returns, contains the value associated with the entry if a value is
/// present; otherwise, returns null.
/// </param>
/// <returns>
/// true if the entry has an associated value; otherwise false.
/// </returns>
bool TryGetValue(out object? value);
/// <summary>
/// Called when the value entry is removed from the value store.
/// </summary>
void Unsubscribe();
}
}

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

@ -0,0 +1,33 @@
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents a typed value entry in an <see cref="IValueFrame"/>.
/// </summary>
internal interface IValueEntry<T> : IValueEntry
{
/// <summary>
/// Gets the property that this value applies to.
/// </summary>
new StyledPropertyBase<T> Property { get; }
/// <summary>
/// Gets the value associated with the entry.
/// </summary>
/// <exception cref="AvaloniaInternalException">
/// The entry has no value.
/// </exception>
new T GetValue();
/// <summary>
/// Tries to get the value associated with the entry.
/// </summary>
/// <param name="value">
/// When this method returns, contains the value associated with the entry if a value is
/// present; otherwise, returns the default value of <typeparamref name="T"/>.
/// </param>
/// <returns>
/// true if the entry has an associated value; otherwise false.
/// </returns>
bool TryGetValue(out T? value);
}
}

56
src/Avalonia.Base/PropertyStore/IValueFrame.cs

@ -0,0 +1,56 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents a collection of property values in a <see cref="PropertyStore.ValueStore"/>.
/// </summary>
/// <remarks>
/// A value frame is an abstraction over the following sources of values in an
/// <see cref="AvaloniaObject"/>:
///
/// - A style
/// - Local values
/// - Animation values
/// </remarks>
internal interface IValueFrame : IDisposable
{
/// <summary>
/// Gets the number of value entries in the frame.
/// </summary>
int EntryCount { get; }
/// <summary>
/// Gets a value indicating whether the frame is active.
/// </summary>
bool IsActive { get; }
/// <summary>
/// Gets the value store that owns the frame.
/// </summary>
ValueStore? Owner { get; }
/// <summary>
/// Gets the frame's priority.
/// </summary>
BindingPriority Priority { get; }
/// <summary>
/// Retreives the frame's value entry with the specified index.
/// </summary>
IValueEntry GetEntry(int index);
/// <summary>
/// Sets the owner of the value frame.
/// </summary>
/// <param name="owner">The new owner.</param>
void SetOwner(ValueStore? owner);
/// <summary>
/// Tries to retreive the frame's value entry for the specified property.
/// </summary>
bool TryGetEntry(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? entry);
}
}

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

@ -0,0 +1,44 @@
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 T GetValue() => _value;
public bool TryGetValue(out T? value)
{
value = _value;
return true;
}
public bool TryGetValue(out object? value)
{
value = _value;
return true;
}
public void Unsubscribe() { }
public void Dispose() => _owner.OnEntryDisposed(this);
object? IValueEntry.GetValue() => _value;
}
}

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

@ -0,0 +1,60 @@
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 : ValueFrameBase
{
public ImmediateValueFrame(BindingPriority priority)
{
Priority = priority;
}
public override bool IsActive => true;
public override BindingPriority Priority { get; }
public BindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
var e = new BindingEntry<T>(this, property, source);
Add(e);
return e;
}
public BindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<T> source)
{
var e = new BindingEntry<T>(this, property, source);
Add(e);
return e;
}
public UntypedBindingEntry<T> AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<object?> source)
{
var e = new UntypedBindingEntry<T>(this, property, source);
Add(e);
return e;
}
public IDisposable 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);
}
}
}

34
src/Avalonia.Base/PropertyStore/InheritanceFrame.cs

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.PropertyStore
{
internal class InheritanceFrame : Dictionary<AvaloniaProperty, EffectiveValue>
{
public InheritanceFrame(ValueStore owner, InheritanceFrame? parent = null)
{
Owner = owner;
Parent = parent;
}
public ValueStore Owner { get; }
public InheritanceFrame? Parent { get; private set; }
public bool TryGetFromThisOrAncestor(AvaloniaProperty property, [NotNullWhen(true)] out EffectiveValue? value)
{
var frame = this;
while (frame is object)
{
if (frame.TryGetValue(property, out value))
return true;
frame = frame.Parent;
}
value = default;
return false;
}
public void SetParent(InheritanceFrame? value) => Parent = value;
}
}

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

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

@ -0,0 +1,61 @@
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;
}
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);
}
}
}
}

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

@ -0,0 +1,61 @@
using System;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Logging;
namespace Avalonia.PropertyStore
{
internal static class LoggingUtils
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogIfNecessary<T>(
AvaloniaObject owner,
AvaloniaProperty property,
BindingValue<T> value)
{
if (value.HasError)
Log(owner, property, value);
}
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<T>(
AvaloniaObject owner,
AvaloniaProperty property,
BindingValue<T> value)
{
owner.GetBindingWarningLogger(property, value.Error)?.Log(
owner,
"Error in binding to {Target}.{Property}: {Message}",
owner,
property,
value.Error!.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);
}
}
}
}

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

@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal class UntypedBindingEntry<T> : IValueEntry<T>,
IObserver<object?>,
IDisposable
{
private readonly ValueFrameBase _frame;
private readonly IObservable<object?> _source;
private IDisposable? _subscription;
private bool _hasValue;
private T? _value;
public UntypedBindingEntry(
ValueFrameBase frame,
StyledPropertyBase<T> property,
IObservable<object?> source)
{
_frame = frame;
_source = source;
Property = property;
}
public bool HasValue
{
get
{
StartIfNecessary();
return _hasValue;
}
}
public StyledPropertyBase<T> Property { get; }
AvaloniaProperty IValueEntry.Property => Property;
public void Dispose()
{
Unsubscribe();
BindingCompleted();
}
public T GetValue()
{
StartIfNecessary();
if (!_hasValue)
throw new AvaloniaInternalException("The binding entry has no value.");
return _value!;
}
public void Start()
{
Debug.Assert(_subscription is null);
// Subscription won't be set until Subscribe completes, but in the meantime we
// need to signal that we've started as Subscribe may cause a value to be produced.
_subscription = Disposable.Empty;
_subscription = _source.Subscribe(this);
}
public bool TryGetValue([MaybeNullWhen(false)] out T value)
{
StartIfNecessary();
value = _value;
return _hasValue;
}
public void OnCompleted() => BindingCompleted();
public void OnError(Exception error) => BindingCompleted();
public void OnNext(object? value) => SetValue(value);
public void OnNext(BindingValue<T> value)
{
if (value.HasValue)
SetValue(value.Value);
else
ClearValue();
}
public void Unsubscribe()
{
_subscription?.Dispose();
_subscription = null;
}
object? IValueEntry.GetValue()
{
StartIfNecessary();
if (!_hasValue)
throw new AvaloniaInternalException("The BindingEntry<T> has no value.");
return _value!;
}
bool IValueEntry.TryGetValue(out object? value)
{
StartIfNecessary();
value = _value;
return _hasValue;
}
private void ClearValue()
{
if (_hasValue)
{
_hasValue = false;
_value = default;
_frame.Owner?.OnBindingValueCleared(Property, _frame.Priority);
}
}
private void SetValue(object? value)
{
if (_frame.Owner is null)
return;
if (value is BindingNotification n)
{
value = n.Value;
}
if (value == AvaloniaProperty.UnsetValue)
{
ClearValue();
}
else if (value == BindingOperations.DoNothing)
{
// Do nothing!
}
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue))
{
if (!_hasValue || !EqualityComparer<T>.Default.Equals(_value, typedValue))
{
_value = typedValue;
_hasValue = true;
_frame.Owner?.OnBindingValueChanged(Property, _frame.Priority, typedValue);
}
}
else
{
ClearValue();
LoggingUtils.LogInvalidValue(_frame.Owner.Owner, Property, typeof(T), value);
}
}
private void BindingCompleted()
{
_subscription = null;
_frame.OnBindingCompleted(this);
}
private void StartIfNecessary()
{
if (_subscription is null)
Start();
}
}
}

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

@ -0,0 +1,37 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore
{
internal static class UntypedValueUtils
{
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;
}
}
}

54
src/Avalonia.Base/PropertyStore/ValueFrameBase.cs

@ -0,0 +1,54 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia.PropertyStore
{
internal abstract class ValueFrameBase : IValueFrame
{
private readonly AvaloniaPropertyValueStore<IValueEntry> _entries = new();
public int EntryCount => _entries.Count;
public abstract bool IsActive { get; }
public ValueStore? Owner { get; private set; }
public abstract BindingPriority Priority { get; }
public bool Contains(AvaloniaProperty property) => _entries.Contains(property);
public IValueEntry GetEntry(int index) => _entries[index];
public void SetOwner(ValueStore? owner) => Owner = owner;
public bool TryGet(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? value)
{
return _entries.TryGetValue(property, out value);
}
public bool TryGetEntry(AvaloniaProperty property, [NotNullWhen(true)] out IValueEntry? entry)
{
return _entries.TryGetValue(property, out entry);
}
public void OnBindingCompleted(IValueEntry binding)
{
Remove(binding.Property);
Owner?.OnBindingCompleted(binding.Property, this);
}
public virtual void Dispose()
{
for (var i = 0; i < _entries.Count; ++i)
_entries[i].Unsubscribe();
}
protected void Add(IValueEntry value)
{
Debug.Assert(!value.Property.IsDirect);
_entries.AddValue(value.Property, value);
}
protected void Remove(AvaloniaProperty property) => _entries.Remove(property);
protected void Set(IValueEntry value) => _entries.SetValue(value.Property, value);
}
}

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

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

@ -0,0 +1,948 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia.Collections.Pooled;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
namespace Avalonia.PropertyStore
{
internal class ValueStore
{
private readonly List<IValueFrame> _frames = new();
private Dictionary<int, IDisposable>? _localValueBindings;
private InheritanceFrame? _inheritanceFrame;
private Dictionary<AvaloniaProperty, EffectiveValue>? _effectiveValues;
private int _frameGeneration;
private int _styling;
public ValueStore(AvaloniaObject owner) => Owner = owner;
public AvaloniaObject Owner { get; }
public IReadOnlyList<IValueFrame> Frames => _frames;
public void BeginStyling() => ++_styling;
public void EndStyling()
{
if (--_styling == 0)
ReevaluateEffectiveValues();
}
public void AddFrame(IValueFrame 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);
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);
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);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
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}.");
}
IDisposable? result = null;
if (priority != BindingPriority.LocalValue)
{
var frame = GetOrCreateImmediateValueFrame(property, priority);
result = frame.AddValue(property, value);
InsertFrame(frame);
}
if (TryGetEffectiveValue(property, out var existing))
{
var effective = (EffectiveValue<T>)existing;
effective.SetAndRaise(this, property, value, priority);
}
else
{
AddEffectiveValueAndRaise(property, value, priority);
}
return result;
}
public object? GetValue(AvaloniaProperty property)
{
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v))
return v.Value;
if (_inheritanceFrame is not null && _inheritanceFrame.TryGetFromThisOrAncestor(property, out v))
return v.Value;
return GetDefaultValue(property);
}
public T GetValue<T>(StyledPropertyBase<T> property)
{
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v))
return ((EffectiveValue<T>)v).Value;
if (_inheritanceFrame is not null && _inheritanceFrame.TryGetFromThisOrAncestor(property, out v))
return ((EffectiveValue<T>)v).Value;
return property.GetDefaultValue(Owner.GetType());
}
public bool IsAnimating(AvaloniaProperty property)
{
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v))
return v.Priority <= BindingPriority.Animation;
return false;
}
public bool IsSet(AvaloniaProperty property)
{
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out var v))
return v.Priority < BindingPriority.Inherited;
return false;
}
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 void SetInheritanceParent(AvaloniaObject? oldParent, AvaloniaObject? newParent)
{
var values = DictionaryPool<AvaloniaProperty, OldNewValue>.Get();
var oldInheritanceFrame = oldParent?.GetValueStore()._inheritanceFrame;
var newInheritanceFrame = newParent?.GetValueStore().OnBecameInheritanceParent();
// The old and new parents are the same, nothing to do here.
if (oldInheritanceFrame == newInheritanceFrame)
return;
// First get the old values from the old inheritance parent.
var f = oldInheritanceFrame;
while (f is not null)
{
foreach (var i in f)
{
values.TryAdd(i.Key, new(i.Value));
}
f = f.Parent;
}
f = newInheritanceFrame;
// Get the new values from the new inheritance parent.
while (f is not null)
{
foreach (var i in f)
{
if (values.TryGetValue(i.Key, out var existing))
values[i.Key] = existing.WithNewValue(i.Value);
else
values.Add(i.Key, new(null, i.Value));
}
f = f.Parent;
}
ParentInheritanceFrameChanged(newInheritanceFrame);
// Raise PropertyChanged events where necessary on this object and inheritance children.
foreach (var i in values)
{
var oldValue = i.Value.OldValue;
var newValue = i.Value.NewValue;
if (oldValue != newValue)
InheritedValueChanged(i.Key, oldValue, newValue);
}
DictionaryPool<AvaloniaProperty, OldNewValue>.Release(values);
}
public void FrameActivationChanged(IValueFrame frame)
{
ReevaluateEffectiveValues();
}
/// <summary>
/// Called by an inheritance child to notify the value store that it has become an
/// inheritance parent. Creates and returns an inheritance frame if necessary.
/// </summary>
/// <returns></returns>
public InheritanceFrame? OnBecameInheritanceParent()
{
if (_inheritanceFrame is not null)
return _inheritanceFrame;
if (_effectiveValues is null)
return null;
foreach (var i in _effectiveValues)
{
if (i.Key.Inherits)
return GetOrCreateInheritanceFrame(true);
}
return null;
}
/// <summary>
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
/// binding produces a new value.
/// </summary>
/// <param name="property">The bound property.</param>
/// <param name="priority">The priority of binding which produced a new value.</param>
/// <param name="value">The new value.</param>
public void OnBindingValueChanged(
AvaloniaProperty property,
BindingPriority priority,
object? value)
{
Debug.Assert(priority != BindingPriority.LocalValue);
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
else
{
AddEffectiveValueAndRaise(property, value, priority);
}
}
/// <summary>
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
/// binding produces a new value.
/// </summary>
/// <param name="property">The bound property.</param>
/// <param name="priority">The priority of binding which produced a new value.</param>
/// <param name="value">The new value.</param>
public void OnBindingValueChanged<T>(
StyledPropertyBase<T> property,
BindingPriority priority,
T value)
{
Debug.Assert(priority != BindingPriority.LocalValue);
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
else
{
AddEffectiveValueAndRaise(property, value, 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="BindingEntry{T}"/> to re-evaluate the effective value when the
/// binding completes or terminates on error.
/// </summary>
/// <param name="property">The previously bound property.</param>
/// <param name="frame">The frame which contained the binding.</param>
public void OnBindingCompleted(AvaloniaProperty property, IValueFrame frame)
{
var priority = frame.Priority;
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
}
/// <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 we have children or an existing inheritance frame, then make sure it's owned and
// set the value. If we have no children and no inheritance frame then it will be
// created when it's needed.
if (children is not null || _inheritanceFrame is not null)
GetOrCreateInheritanceFrame(true)[property] = value;
if (children is not null)
{
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnParentInheritedValueChanged(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);
Debug.Assert(_inheritanceFrame is null || _inheritanceFrame.Owner == this);
if (_inheritanceFrame is null || _inheritanceFrame.Owner != this)
return;
_inheritanceFrame.Remove(property);
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().OnParentInheritedValueChanged(property, oldValue, defaultValue);
}
}
}
/// <summary>
/// Called when a <see cref="LocalValueBindingObserver{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);
}
}
}
public void OnParentInheritedValueChanged<T>(
StyledPropertyBase<T> property,
T oldValue,
T newValue)
{
Debug.Assert(property.Inherits);
// Ensure the inheritance frame is created.
GetOrCreateInheritanceFrame(false);
// If the inherited value is set locally, propagation stops here.
if (_effectiveValues is not null && _effectiveValues.ContainsKey(property))
return;
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().OnParentInheritedValueChanged(property, oldValue, newValue);
}
}
/// <summary>
/// Called by an <see cref="IValueFrame"/> 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(IValueFrame frame, AvaloniaProperty property)
{
Debug.Assert(frame.IsActive);
if (TryGetEffectiveValue(property, out var existing))
{
if (frame.Priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
else
{
Logger.TryGet(LogEventLevel.Error, LogArea.Property)?.Log(
Owner,
"Internal error: ValueStore.OnEntryRemoved called for {Property} " +
"but no effective value was found.",
property);
Debug.Assert(false);
}
}
public bool RemoveFrame(IValueFrame frame)
{
if (_frames.Remove(frame))
{
frame.Dispose();
++_frameGeneration;
ReevaluateEffectiveValues();
}
return false;
}
public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property)
{
var effective = GetEffectiveValue(property);
return new AvaloniaPropertyValue(
property,
effective?.Value,
effective?.Priority ?? BindingPriority.Unset,
null);
}
private void InsertFrame(IValueFrame frame)
{
var index = _frames.BinarySearch(frame, FrameInsertionComparer.Instance);
if (index < 0)
index = ~index;
_frames.Insert(index, frame);
++_frameGeneration;
frame.SetOwner(this);
}
private InheritanceFrame GetOrCreateInheritanceFrame(bool owned)
{
if (_inheritanceFrame is null)
{
var parentFrame = Owner.InheritanceParent?.GetValueStore()._inheritanceFrame;
_inheritanceFrame = owned || parentFrame is null ?
new(this, parentFrame) :
parentFrame;
if (_effectiveValues is not null)
{
foreach (var i in _effectiveValues)
{
if (i.Key.Inherits)
_inheritanceFrame[i.Key] = i.Value;
}
}
}
else if (owned && _inheritanceFrame.Owner != this)
{
_inheritanceFrame = new(this, _inheritanceFrame);
}
return _inheritanceFrame;
}
private ImmediateValueFrame GetOrCreateImmediateValueFrame(
AvaloniaProperty property,
BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
// TODO: Binary search?
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
if (frame is ImmediateValueFrame immediate && !immediate.Contains(property))
return immediate;
if (frame.Priority > priority)
break;
}
var result = new ImmediateValueFrame(priority);
InsertFrame(result);
return result;
}
private void ReevaluateEffectiveValue(
AvaloniaProperty property,
EffectiveValue current,
bool ignoreLocalValue = false)
{
if (EvaluateEffectiveValue(
property,
!ignoreLocalValue ? current : null,
out var value,
out var priority,
out var baseValue,
out var basePriority))
{
if (basePriority != BindingPriority.Unset)
current.SetAndRaise(this, property, value, priority, baseValue, basePriority);
else
current.SetAndRaise(this, property, value, priority);
}
else
{
_effectiveValues?.Remove(property);
current.DisposeAndRaiseUnset(this, property);
}
}
/// <summary>
/// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
/// event and notifies inheritance children if necessary .
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="property">The property.</param>
/// <param name="value">The property value.</param>
/// <param name="priority">The value priority.</param>
private void AddEffectiveValueAndRaise(AvaloniaProperty property, object? value, BindingPriority priority)
{
Debug.Assert(priority < BindingPriority.Inherited);
var effectiveValue = property.CreateEffectiveValue(Owner);
_effectiveValues ??= new();
_effectiveValues.Add(property, effectiveValue);
effectiveValue.SetAndRaise(this, property, value, priority);
}
/// <summary>
/// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
/// event and notifies inheritance children if necessary .
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="property">The property.</param>
/// <param name="value">The property value.</param>
/// <param name="priority">The value priority.</param>
private void AddEffectiveValueAndRaise<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
{
Debug.Assert(priority < BindingPriority.Inherited);
var defaultValue = property.GetDefaultValue(Owner.GetType());
var effectiveValue = new EffectiveValue<T>(defaultValue, BindingPriority.Unset);
_effectiveValues ??= new();
_effectiveValues.Add(property, effectiveValue);
effectiveValue.SetAndRaise(this, property, value, priority);
}
/// <summary>
/// Evaluates the current value and base value for a property based on the current frames and optionally
/// local values. Does not evaluate inherited values.
/// </summary>
/// <param name="property">The property to evaluation</param>
/// <param name="current">The current effective value if the local value is to be considered.</param>
/// <param name="value">When the method exits will contain the current value if it exists.</param>
/// <param name="priority">When the method exits will contain the current value priority.</param>
/// <param name="baseValue">>When the method exits will contain the current base value if it exists.</param>
/// <param name="basePriority">When the method exits will contain the current base value priority.</param>
/// <returns>
/// True if a value was found, otherwise false.
/// </returns>
private bool EvaluateEffectiveValue(
AvaloniaProperty property,
EffectiveValue? current,
out object? value,
out BindingPriority priority,
out object? baseValue,
out BindingPriority basePriority)
{
var i = _frames.Count - 1;
value = baseValue = AvaloniaProperty.UnsetValue;
priority = basePriority = BindingPriority.Unset;
// First try to find an animation value.
for (; i >= 0; --i)
{
var frame = _frames[i];
if (frame.Priority > BindingPriority.Animation)
break;
if (frame.IsActive &&
frame.TryGetEntry(property, out var entry) &&
entry.TryGetValue(out value))
{
priority = frame.Priority;
--i;
break;
}
}
// Local values come from the current EffectiveValue.
if (current?.Priority == BindingPriority.LocalValue)
{
// If there's a current effective local value and no animated value then we use the
// effective local value.
if (priority == BindingPriority.Unset)
{
value = current.Value;
priority = BindingPriority.LocalValue;
}
// The local value is always the base value.
baseValue = current.Value;
basePriority = BindingPriority.LocalValue;
return true;
}
// Or the current effective base value if there's no longer an animated value.
if (priority == BindingPriority.Unset && current?.BasePriority == BindingPriority.LocalValue)
{
value = baseValue = current.BaseValue;
priority = basePriority = BindingPriority.LocalValue;
return true;
}
// Now try the rest of the frames.
for (; i >= 0; --i)
{
var frame = _frames[i];
if (frame.IsActive &&
frame.TryGetEntry(property, out var entry) &&
entry.TryGetValue(out var v))
{
if (priority == BindingPriority.Unset)
{
value = v;
priority = frame.Priority;
}
baseValue = v;
basePriority = frame.Priority;
return true;
}
}
return priority != BindingPriority.Unset;
}
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;
// 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 ParentInheritanceFrameChanged(InheritanceFrame? parent)
{
if (_inheritanceFrame?.Owner == this)
{
_inheritanceFrame.SetParent(parent);
}
else if (_inheritanceFrame != parent)
{
_inheritanceFrame = parent;
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().ParentInheritanceFrameChanged(parent);
}
}
}
private void ReevaluateEffectiveValues()
{
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;
// Reset all non-LocalValue effective values to Unset priority.
if (_effectiveValues is not null)
{
foreach (var v in _effectiveValues)
{
var e = v.Value;
if (e.Priority != BindingPriority.LocalValue)
e.SetPriority(BindingPriority.Unset);
if (e.BasePriority != BindingPriority.LocalValue)
e.SetBasePriority(BindingPriority.Unset);
}
}
// 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;
var count = frame.EntryCount;
for (var j = 0; j < count; ++j)
{
var entry = frame.GetEntry(j);
if (!entry.HasValue)
continue;
var property = entry.Property;
if (_effectiveValues is not null &&
_effectiveValues.TryGetValue(property, out var effectiveValue))
{
if (effectiveValue.Priority == BindingPriority.Unset ||
effectiveValue.BasePriority == BindingPriority.Unset)
{
effectiveValue.SetAndRaise(this, entry, priority);
}
}
else
{
var v = property.CreateEffectiveValue(Owner);
_effectiveValues ??= new();
_effectiveValues.Add(property, v);
v.SetAndRaise(this, entry, priority);
}
if (generation != _frameGeneration)
goto restart;
}
}
// Remove all effective values that are still unset.
if (_effectiveValues is not null)
{
PooledList<AvaloniaProperty>? remove = null;
foreach (var v in _effectiveValues)
{
var e = v.Value;
if (e.Priority == BindingPriority.Unset)
{
remove ??= new();
remove.Add(v.Key);
}
}
if (remove is not null)
{
foreach (var v in remove)
{
if (_effectiveValues.Remove(v, out var e))
e.DisposeAndRaiseUnset(this, v);
}
remove.Dispose();
}
}
}
[MemberNotNullWhen(true, nameof(_effectiveValues))]
private bool TryGetEffectiveValue(
AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? value)
{
if (_effectiveValues is not null && _effectiveValues.TryGetValue(property, out value))
return true;
value = null;
return false;
}
private EffectiveValue? GetEffectiveValue(AvaloniaProperty property)
{
if (_effectiveValues is not null && _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 class FrameInsertionComparer : IComparer<IValueFrame>
{
public static readonly FrameInsertionComparer Instance = new FrameInsertionComparer();
public int Compare(IValueFrame? x, IValueFrame? y)
{
var result = y!.Priority - x!.Priority;
return result != 0 ? result : -1;
}
}
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);
}
}
}

130
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;
@ -351,15 +351,21 @@ namespace Avalonia
{
if (_initCount == 0 && !_styled)
{
try
{
BeginBatchUpdate();
AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
}
finally
var styler = AvaloniaLocator.Current.GetService<IStyler>();
if (styler is object)
{
_styled = true;
EndBatchUpdate();
GetValueStore().BeginStyling();
try
{
styler.ApplyStyles(this);
}
finally
{
_styled = true;
GetValueStore().EndStyling();
}
}
if (_hasPromotedTheme)
@ -389,14 +395,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 +529,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();
@ -830,56 +825,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])
{
applied.Dispose();
_appliedStyles.RemoveAt(j);
}
}
}
}
private void InvalidateStylesOnThisAndDescendents()
{
InvalidateStyles();
@ -895,7 +859,7 @@ namespace Avalonia
}
}
private void DetachStylesFromThisAndDescendents(IReadOnlyList<IStyle> styles)
private void DetachStylesFromThisAndDescendents(IReadOnlyList<StyleBase> styles)
{
DetachStyles(styles);
@ -927,38 +891,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);
}
}
}

84
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>(GetDefaultValue(o.GetType()), BindingPriority.Unset);
}
/// <inheritdoc/>
internal override void RouteClearValue(AvaloniaObject o)
{
@ -182,34 +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;
internal override IDisposable RouteBind(
AvaloniaObject target,
IObservable<object?> source,
BindingPriority priority)
{
return target.Bind<TValue>(this, source, priority);
}
/// <inheritdoc/>
@ -222,39 +249,6 @@ namespace Avalonia
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!);
}
}
private object? GetDefaultBoxedValue(Type type)
{
_ = type ?? throw new ArgumentNullException(nameof(type));

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

@ -16,6 +16,23 @@ namespace Avalonia.Styling.Activators
public int Count => _sources?.Count ?? 0;
public override bool IsActive
{
get
{
if (_sources is null)
return false;
foreach (var source in _sources)
{
if (!source.IsActive)
return false;
}
return true;
}
}
public void Add(IStyleActivator activator)
{
_sources ??= new List<IStyleActivator>();

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

@ -18,6 +18,11 @@ namespace Avalonia.Styling.Activators
[Unstable]
public interface IStyleActivator : IDisposable
{
/// <summary>
/// Gets a value indicating whether the style is activated.
/// </summary>
bool IsActive { get; }
/// <summary>
/// Subscribes to the activator.
/// </summary>

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

@ -9,6 +9,7 @@ namespace Avalonia.Styling.Activators
{
private readonly IStyleActivator _source;
public NotActivator(IStyleActivator source) => _source = source;
public override bool IsActive => !_source.IsActive;
void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value);
protected override void Initialize() => _source.Subscribe(this, 0);
protected override void Deinitialize() => _source.Unsubscribe(this);

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

@ -26,9 +26,11 @@ namespace Avalonia.Styling.Activators
_reversed = reversed;
}
public override bool IsActive => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
protected override void Initialize()
{
PublishNext(IsMatching());
PublishNext(IsActive);
_provider.ChildIndexChanged += ChildIndexChanged;
}
@ -47,10 +49,8 @@ namespace Avalonia.Styling.Activators
|| e.Child is null
|| e.Child == _control)
{
PublishNext(IsMatching());
PublishNext(IsActive);
}
}
private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
}
}

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

@ -16,6 +16,23 @@ namespace Avalonia.Styling.Activators
public int Count => _sources?.Count ?? 0;
public override bool IsActive
{
get
{
if (_sources is null)
return false;
foreach (var source in _sources)
{
if (source.IsActive)
return true;
}
return false;
}
}
public void Add(IStyleActivator activator)
{
_sources ??= new List<IStyleActivator>();

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

@ -24,6 +24,15 @@ namespace Avalonia.Styling.Activators
_value = value;
}
public override bool IsActive
{
get
{
var value = _control.GetValue(_property);
return PropertyEqualsSelector.Compare(_property.PropertyType, value, _value);
}
}
protected override void Initialize()
{
_subscription = _control.GetObservable(_property).Subscribe(this);
@ -33,6 +42,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) => PublishNext(IsActive);
}
}

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

@ -1,5 +1,3 @@
#nullable enable
namespace Avalonia.Styling.Activators
{
/// <summary>
@ -11,6 +9,8 @@ namespace Avalonia.Styling.Activators
private int _tag;
private bool? _value;
public abstract bool IsActive { get; }
public void Subscribe(IStyleActivatorSink sink, int tag = 0)
{
if (_sink is null)

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

@ -22,6 +22,8 @@ namespace Avalonia.Styling.Activators
_match = match;
}
public override bool IsActive => AreClassesMatching(_classes, _match);
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
{
int remainingMatches = toMatch.Count;
@ -54,12 +56,12 @@ namespace Avalonia.Styling.Activators
void IClassesChangedListener.Changed()
{
PublishNext(IsMatching());
PublishNext(IsActive);
}
protected override void Initialize()
{
PublishNext(IsMatching());
PublishNext(IsActive);
_classes.AddListener(this);
}
@ -67,7 +69,5 @@ namespace Avalonia.Styling.Activators
{
_classes.RemoveListener(this);
}
private bool IsMatching() => AreClassesMatching(_classes, _match);
}
}

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

@ -1,4 +1,5 @@
using System;
using Avalonia.PropertyStore;
namespace Avalonia.Styling
{

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

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

@ -1,200 +1,41 @@
using System;
using System.Reactive.Subjects;
using System.Reactive.Linq;
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 : BindingEntry, 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;
public PropertySetterBindingInstance(
IStyleable target,
StyledPropertyBase<T> property,
IBinding binding)
{
_target = target;
_styledProperty = property;
_binding = binding.Initiate(_target, property);
if (_binding?.Mode == BindingMode.OneTime)
{
// 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.");
}
_inner = new Inner(this);
}
private readonly IDisposable? _twoWaySubscription;
public PropertySetterBindingInstance(
IStyleable target,
DirectPropertyBase<T> property,
IBinding binding)
{
_target = target;
_directProperty = property;
_binding = binding.Initiate(_target, property);
_inner = new Inner(this);
}
public void Start(bool hasActivator)
{
if (_binding is null)
return;
_isActive = !hasActivator;
if (_styledProperty is object)
{
if (_binding.Mode != BindingMode.OneWayToSource)
{
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
_subscription = _target.Bind(_styledProperty, this, priority);
}
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);
}
}
}
public void Activate()
{
if (_binding is null)
return;
if (!_isActive)
{
_innerSubscription ??= _binding.Observable!.Subscribe(_inner);
_isActive = true;
PublishNext();
}
}
public void Deactivate()
AvaloniaObject target,
StyleInstance instance,
AvaloniaProperty property,
BindingMode mode,
IObservable<object?> source)
: base(instance, property, source)
{
if (_isActive)
if (mode == BindingMode.TwoWay)
{
_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)
// TODO: HUGE HACK FIXME
if (source is IObserver<object?> observer)
{
_innerSubscription ??= _binding.Observable!.Subscribe(_inner);
_twoWaySubscription = target.GetObservable(property).Skip(1).Subscribe(observer);
}
else
{
PublishNext();
throw new NotSupportedException(
"Attempting to bind two-way with a binding source which doesn't support it.");
}
}
}
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?>
public override void Unsubscribe()
{
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);
_twoWaySubscription?.Dispose();
base.Unsubscribe();
}
}
}

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

@ -1,127 +1,34 @@
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 bool HasValue => true;
public AvaloniaProperty Property { get; }
public void Activate()
public object? GetValue()
{
if (!_isActive)
{
_isActive = true;
PublishNext();
}
TryGetValue(out var value);
return value;
}
public void Deactivate()
public bool TryGetValue(out object? value)
{
if (_isActive)
{
_isActive = false;
PublishNext();
}
value = _value ??= _template.Build();
return value != AvaloniaProperty.UnsetValue;
}
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();
}
protected override void Subscribed() => PublishNext();
protected override void Unsubscribed() { }
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() { }
}
}

80
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,78 @@ 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;
bool IValueEntry.TryGetValue(out object? value)
{
value = Value;
return true;
}
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();
}
}
}

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

@ -1,4 +1,5 @@
using System;
using Avalonia.PropertyStore;
namespace Avalonia.Styling
{
@ -7,6 +8,7 @@ namespace Avalonia.Styling
/// </summary>
public class Style : StyleBase
{
private bool? _inControlTheme;
private Selector? _selector;
/// <summary>
@ -48,7 +50,9 @@ namespace Avalonia.Styling
SelectorMatch.NeverThisInstance);
if (match.IsMatch)
{
Attach(target, match.Activator);
}
result = match.Result;
}
@ -95,6 +99,28 @@ namespace Avalonia.Styling
base.SetParent(parent);
}
private bool IsInControlTheme()
{
if (_inControlTheme.HasValue)
return _inControlTheme.Value;
StyleBase? s = this;
while (s is not null)
{
if (s is ControlTheme)
{
_inControlTheme = true;
return true;
}
s = s.Parent as StyleBase;
}
_inControlTheme = false;
return false;
}
private static Selector? ValidateSelector(Selector? selector)
{
if (selector is TemplateSelector)

22
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
@ -80,11 +81,24 @@ namespace Avalonia.Styling
return _resources?.TryGetResource(key, out result) ?? false;
}
internal void Attach(IStyleable target, IStyleActivator? activator)
internal IValueFrame 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.");
var instance = new StyleInstance(this, activator);
if (_setters is object)
{
foreach (var setter in _setters)
{
var setterInstance = setter.Instance(instance, target);
instance.Add(setterInstance);
}
}
ao.GetValueStore().AddFrame(instance);
return instance;
}
internal SelectorMatchResult TryAttachChildren(IStyleable target, object? host)

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

@ -1,137 +1,74 @@
using System;
using System.Collections.Generic;
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"/> implements the <see cref="IValueFrame"/> interface 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 : ValueFrameBase, IStyleInstance, IStyleActivatorSink, IDisposable
{
private readonly ISetterInstance[]? _setters;
private readonly IDisposable[]? _animations;
private readonly IStyleActivator? _activator;
private readonly Subject<bool>? _animationTrigger;
private List<ISetterInstance>? _setters;
private bool _isActivatorInitializing;
private bool _isActivatorSubscribed;
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];
Priority = activator is object ? BindingPriority.StyleTrigger : BindingPriority.Style;
Source = style;
}
for (var i = 0; i < setterCount; ++i)
{
_setters[i] = setters[i].Instance(Target);
}
}
public bool HasActivator => _activator is object;
if (animations is not null && target is Animatable animatable)
public override bool IsActive
{
get
{
var animationsCount = animations.Count;
_animations = new IDisposable[animationsCount];
_animationTrigger = new Subject<bool>();
for (var i = 0; i < animationsCount; ++i)
if (_activator is object && !_isActivatorSubscribed)
{
_animations[i] = animations[i].Apply(animatable, null, _animationTrigger);
_isActivatorInitializing = true;
_activator.Subscribe(this);
_isActivatorInitializing = false;
_isActivatorSubscribed = true;
}
return _activator?.IsActive ?? true;
}
}
public bool HasActivator => _activator is not null;
public bool IsActive { get; private set; }
public override BindingPriority Priority { get; }
public IStyle Source { get; }
public IStyleable Target { get; }
public void Start()
public void Add(ISetterInstance instance)
{
var hasActivator = HasActivator;
if (_setters is not null)
{
foreach (var setter in _setters)
{
setter.Start(hasActivator);
}
}
if (hasActivator)
{
_activator!.Subscribe(this, 0);
}
else if (_animationTrigger is not null)
{
_animationTrigger.OnNext(true);
}
if (instance is IValueEntry valueEntry)
base.Add(valueEntry);
else
(_setters ??= new()).Add(instance);
}
public void Dispose()
public override void Dispose()
{
if (_setters is not null)
{
foreach (var setter in _setters)
{
setter.Dispose();
}
}
if (_animations is not null)
{
foreach (var subscription in _animations)
{
subscription.Dispose();
}
}
base.Dispose();
_activator?.Dispose();
}
private void ActivatorChanged(bool value)
void IStyleActivatorSink.OnNext(bool value, int tag)
{
if (IsActive != value)
{
IsActive = value;
_animationTrigger?.OnNext(value);
if (_setters is not null)
{
if (IsActive)
{
foreach (var setter in _setters)
{
setter.Activate();
}
}
else
{
foreach (var setter in _setters)
{
setter.Deactivate();
}
}
}
}
if (!_isActivatorInitializing)
Owner?.FrameActivationChanged(this);
}
void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value);
}
}

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

@ -92,6 +92,8 @@ namespace Avalonia.Utilities
return (0, false);
}
public bool Contains(AvaloniaProperty property) => TryFindEntry(property.Id).Item2;
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
@ -129,7 +131,12 @@ namespace Avalonia.Utilities
public void SetValue(AvaloniaProperty property, TValue value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
var (index, found) = TryFindEntry(property.Id);
if (found)
_entries[index].Value = value;
else
AddValue(property, value);
}
public void Remove(AvaloniaProperty property)

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

@ -552,27 +552,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>

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

@ -371,11 +371,11 @@ namespace Avalonia.Controls.Primitives
{
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
{
foreach (var child in this.GetTemplateChildren())
child.InvalidateStyles();
}
//if (change.Property == ThemeProperty)
//{
// foreach (var child in this.GetTemplateChildren())
// child.InvalidateStyles();
//}
}
/// <summary>

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

@ -413,25 +413,33 @@ 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();
}
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;
}
}
}
}

372
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.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));
}
@ -404,7 +490,7 @@ namespace Avalonia.Base.UnitTests
public void Observable_Is_Unsubscribed_When_Subscription_Disposed()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable<string>();
var source = scheduler.CreateColdObservable<BindingValue<string>>();
var target = new Class1();
var subscription = target.Bind(Class1.FooProperty, source);
@ -482,7 +568,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 +746,76 @@ 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}: 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, 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 +882,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));
}
@ -786,7 +943,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 +954,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]
@ -822,7 +979,7 @@ namespace Avalonia.Base.UnitTests
public void Disposing_Completed_Binding_Does_Not_Throw()
{
var target = new Class1();
var source = new Subject<string>();
var source = new Subject<BindingValue<string>>();
var subscription = target.Bind(Class1.FooProperty, source);
source.OnCompleted();
@ -830,68 +987,15 @@ namespace Avalonia.Base.UnitTests
subscription.Dispose();
}
[Fact]
public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_With_Value()
{
var target = new Class1();
var source = new TestTwoWayBindingViewModel() { Value = 1 };
source.ResetSetterCalled();
target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source });
Assert.False(source.ValueSetterCalled);
}
[Fact]
public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer_With_Value()
{
var target = new Class1();
var source = new TestTwoWayBindingViewModel() { [0] = 1 };
source.ResetSetterCalled();
target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source });
Assert.False(source.ValueSetterCalled);
}
[Fact]
public void Disposing_a_TwoWay_Binding_Should_Set_Default_Value_On_Binding_Target_But_Not_On_Source()
{
var target = new Class3();
// 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 };
// Reset the setter counter
source.ResetSetterCalled();
// 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 });
// Dispose the minimum binding
disposable_1.Dispose();
// Dispose the value binding
disposable_2.Dispose();
// 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);
}
/// <summary>
/// Returns an observable that returns a single value but does not complete.
/// </summary>
/// <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 +1022,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 +1033,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 +1049,7 @@ namespace Avalonia.Base.UnitTests
private double _value;
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public double Value
{
@ -1008,8 +1062,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 +1088,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 +1098,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; }
}
}
}

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

120
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,17 @@ 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 };
@ -17,7 +28,23 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void Setting_InheritanceParent_Raises_PropertyChanged_When_Value_Changed_In_Parent()
public void ClearValue_Clears_Inherited_Value()
{
Class1 parent = new Class1();
Class2 child = new Class2 { Parent = parent };
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 Setting_InheritanceParent_Raises_PropertyChanged_When_Parent_Has_Value_Set()
{
bool raised = false;
@ -29,15 +56,17 @@ 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_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 +83,7 @@ namespace Avalonia.Base.UnitTests
child.Parent = parent;
Assert.True(raised);
Assert.Equal("changed", child.GetValue(AttachedOwner.AttachedProperty));
}
[Fact]
@ -71,6 +101,7 @@ namespace Avalonia.Base.UnitTests
child.Parent = parent;
Assert.False(raised);
Assert.Equal("localvalue", child.GetValue(Class1.BazProperty));
}
[Fact]
@ -91,6 +122,7 @@ namespace Avalonia.Base.UnitTests
parent.SetValue(Class1.BazProperty, "changed");
Assert.True(raised);
Assert.Equal("changed", child.GetValue(Class1.BazProperty));
}
[Fact]
@ -111,6 +143,7 @@ namespace Avalonia.Base.UnitTests
parent.SetValue(AttachedOwner.AttachedProperty, "changed");
Assert.True(raised);
Assert.Equal("changed", child.GetValue(AttachedOwner.AttachedProperty));
}
[Fact]
@ -128,6 +161,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 =

51
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);
@ -48,47 +48,12 @@ namespace Avalonia.Base.UnitTests
Assert.False(change.IsEffectiveValueChange);
}
[Fact]
public void OnPropertyChangedCore_Is_Called_On_All_Binding_Property_Changes()
{
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());
}
[Fact]
public void OnPropertyChanged_Is_Called_Only_For_Effective_Value_Changes()
{
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 +89,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()
{

16
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,28 +150,31 @@ namespace Avalonia.Base.UnitTests
internal override IDisposable RouteBind(
AvaloniaObject o,
IObservable<BindingValue<object>> source,
IObservable<object> source,
BindingPriority priority)
{
throw new NotImplementedException();
}
internal override void RouteClearValue(AvaloniaObject o)
internal override IDisposable RouteBind(
AvaloniaObject o,
IObservable<BindingValue<object>> source,
BindingPriority priority)
{
throw new NotImplementedException();
}
internal override object RouteGetValue(AvaloniaObject o)
internal override void RouteClearValue(AvaloniaObject o)
{
throw new NotImplementedException();
}
internal override object RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority)
internal override object RouteGetValue(AvaloniaObject o)
{
throw new NotImplementedException();
}
internal override void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject oldParent)
internal override object RouteGetBaseValue(AvaloniaObject o)
{
throw new NotImplementedException();
}
@ -183,7 +187,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();
}

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

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

@ -0,0 +1,126 @@
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Subjects;
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.FooProperty, "foo", "foodefault"),
new(Class1.BarProperty, "bar", "bardefault"),
}, result);
}
[Fact]
public void Removing_Frame_Unsubscribes_Binding()
{
var target = new Class1();
var scheduler = new TestScheduler();
var obs = scheduler.CreateColdObservable(OnNext(0, "bar"));
var result = new List<PropertyChange>();
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);
}
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);
}
}

319
tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

@ -5,11 +5,14 @@ using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Diagnostics;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
using Xunit;
#nullable enable
namespace Avalonia.Base.UnitTests.Styling
{
public class SetterTests
@ -28,13 +31,13 @@ namespace Avalonia.Base.UnitTests.Styling
var control = new TextBlock();
var subject = new BehaviorSubject<object>("foo");
var descriptor = InstancedBinding.OneWay(subject);
var binding = Mock.Of<IBinding>(x => x.Initiate(control, TextBlock.TextProperty, null, false) == descriptor);
var binding = Mock.Of<IBinding>(x => x.Initiate(control, TextBlock.TagProperty, null, false) == descriptor);
var style = Mock.Of<IStyle>();
var setter = new Setter(TextBlock.TextProperty, binding);
var setter = new Setter(TextBlock.TagProperty, binding);
setter.Instance(control).Start(false);
Apply(setter, control);
Assert.Equal("foo", control.Text);
Assert.Equal("foo", control.Tag);
}
[Fact]
@ -47,7 +50,7 @@ namespace Avalonia.Base.UnitTests.Styling
var style = Mock.Of<IStyle>();
var setter = new Setter(TextBlock.TagProperty, binding);
setter.Instance(control).Start(false);
Apply(setter, control);
Assert.Equal(null, control.Text);
}
@ -60,133 +63,309 @@ namespace Avalonia.Base.UnitTests.Styling
var style = Mock.Of<IStyle>();
var setter = new Setter(Decorator.ChildProperty, template);
setter.Instance(control).Start(false);
Apply(setter, control);
Assert.IsType<Canvas>(control.Child);
}
[Fact]
public void Can_Set_Direct_Property_In_Style_Without_Activator()
{
var control = new TextBlock();
var target = new Setter();
var style = new Style(x => x.Is<TextBlock>())
{
Setters =
{
new Setter(TextBlock.TextProperty, "foo"),
}
};
Apply(style, control);
Assert.Equal("foo", control.Text);
}
[Fact]
public void Can_Set_Direct_Property_Binding_In_Style_Without_Activator()
{
var control = new TextBlock();
var target = new Setter();
var source = new BehaviorSubject<object?>("foo");
var style = new Style(x => x.Is<TextBlock>())
{
Setters =
{
new Setter(TextBlock.TextProperty, source.ToBinding()),
}
};
Apply(style, control);
Assert.Equal("foo", control.Text);
}
[Fact]
public void Cannot_Set_Direct_Property_Binding_In_Style_With_Activator()
{
var control = new TextBlock();
var target = new Setter();
var source = new BehaviorSubject<object?>("foo");
var style = new Style(x => x.Is<TextBlock>().Class("foo"))
{
Setters =
{
new Setter(TextBlock.TextProperty, source.ToBinding()),
}
};
Assert.Throws<InvalidOperationException>(() => Apply(style, control));
}
[Fact]
public void Cannot_Set_Direct_Property_In_Style_With_Activator()
{
var control = new TextBlock();
var target = new Setter();
var style = new Style(x => x.Is<TextBlock>().Class("foo"))
{
Setters =
{
new Setter(TextBlock.TextProperty, "foo"),
}
};
Assert.Throws<InvalidOperationException>(() => Apply(style, control));
}
[Fact]
public void Does_Not_Call_Converter_ConvertBack_On_OneWay_Binding()
{
var control = new Decorator { Name = "foo" };
var style = Mock.Of<IStyle>();
var control = new Decorator
{
Name = "foo",
Classes = { "foo" },
};
var binding = new Binding("Name", BindingMode.OneWay)
{
Converter = new TestConverter(),
RelativeSource = new RelativeSource(RelativeSourceMode.Self),
};
var setter = new Setter(Decorator.TagProperty, binding);
var instance = setter.Instance(control);
instance.Start(true);
instance.Activate();
var style = new Style(x => x.OfType<Decorator>().Class("foo"))
{
Setters =
{
new Setter(Decorator.TagProperty, binding)
},
};
Apply(style, control);
Assert.Equal("foobar", control.Tag);
// Issue #1218 caused TestConverter.ConvertBack to throw here.
instance.Deactivate();
control.Classes.Remove("foo");
Assert.Null(control.Tag);
}
[Fact]
public void Setter_Should_Apply_Value_Without_Activator_With_Style_Priority()
{
var control = new Control();
var setter = new Setter(TextBlock.TagProperty, "foo");
var control = new Border();
var style = new Style(x => x.OfType<Border>())
{
Setters =
{
new Setter(Control.TagProperty, "foo"),
},
};
var raised = 0;
setter.Instance(control).Start(false);
control.PropertyChanged += (s, e) =>
{
Assert.Equal(Control.TagProperty, e.Property);
Assert.Equal(BindingPriority.Style, e.Priority);
++raised;
};
Assert.Equal("foo", control.Tag);
Assert.Equal(BindingPriority.Style, control.GetDiagnostic(TextBlock.TagProperty).Priority);
Apply(style, control);
Assert.Equal(1, raised);
}
[Fact]
public void Setter_Should_Apply_Value_With_Activator_As_Binding_With_StyleTrigger_Priority()
public void Setter_Should_Apply_Value_With_Activator_With_StyleTrigger_Priority()
{
var control = new Canvas();
var setter = new Setter(TextBlock.TagProperty, "foo");
var control = new Border { Classes = { "foo" } };
var style = new Style(x => x.OfType<Border>().Class("foo"))
{
Setters =
{
new Setter(Control.TagProperty, "foo"),
},
};
var activator = new Subject<bool>();
var raised = 0;
control.PropertyChanged += (s, e) =>
{
Assert.Equal(Border.TagProperty, e.Property);
Assert.Equal(BindingPriority.StyleTrigger, e.Priority);
++raised;
};
var instance = setter.Instance(control);
instance.Start(true);
instance.Activate();
Apply(style, control);
Assert.Equal("foo", control.Tag);
Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority);
Assert.Equal(1, raised);
}
[Fact]
public void Setter_Should_Apply_Binding_Without_Activator_With_Style_Priority()
{
var control = new Canvas();
var source = new { Foo = "foo" };
var setter = new Setter(TextBlock.TagProperty, new Binding
var control = new Border
{
Source = source,
Path = nameof(source.Foo),
});
DataContext = "foo",
};
setter.Instance(control).Start(false);
var style = new Style(x => x.OfType<Border>())
{
Setters =
{
new Setter(Control.TagProperty, new Binding()),
},
};
Assert.Equal("foo", control.Tag);
Assert.Equal(BindingPriority.Style, control.GetDiagnostic(TextBlock.TagProperty).Priority);
var raised = 0;
control.PropertyChanged += (s, e) =>
{
Assert.Equal(Control.TagProperty, e.Property);
Assert.Equal(BindingPriority.Style, e.Priority);
++raised;
};
Apply(style, control);
Assert.Equal(1, raised);
}
[Fact]
public void Setter_Should_Apply_Binding_With_Activator_With_StyleTrigger_Priority()
{
var control = new Canvas();
var source = new { Foo = "foo" };
var setter = new Setter(TextBlock.TagProperty, new Binding
var control = new Border
{
Source = source,
Path = nameof(source.Foo),
});
Classes = { "foo" },
DataContext = "foo",
};
var instance = setter.Instance(control);
instance.Start(true);
instance.Activate();
var style = new Style(x => x.OfType<Border>().Class("foo"))
{
Setters =
{
new Setter(Control.TagProperty, new Binding()),
},
};
Assert.Equal("foo", control.Tag);
Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority);
var raised = 0;
control.PropertyChanged += (s, e) =>
{
Assert.Equal(Control.TagProperty, e.Property);
Assert.Equal(BindingPriority.StyleTrigger, e.Priority);
++raised;
};
Apply(style, control);
Assert.Equal(1, raised);
}
[Fact]
public void Disposing_Setter_Should_Preserve_LocalValue()
public void Direct_Property_Setter_With_TwoWay_Binding_Should_Update_Source()
{
var control = new Canvas();
var setter = new Setter(TextBlock.TagProperty, "foo");
var instance = setter.Instance(control);
instance.Start(true);
instance.Activate();
using var app = UnitTestApplication.Start(TestServices.MockThreadingInterface);
var data = new Data { Foo = "foo" };
var control = new TextBox
{
DataContext = data,
};
control.Tag = "bar";
var style = new Style(x => x.OfType<TextBox>())
{
Setters =
{
new Setter
{
Property = TextBox.TextProperty,
Value = new Binding
{
Path = "Foo",
Mode = BindingMode.TwoWay
}
}
},
};
instance.Dispose();
Apply(style, control);
Assert.Equal("foo", control.Text);
Assert.Equal("bar", control.Tag);
control.Text = "bar";
Assert.Equal("bar", data.Foo);
}
[Fact]
public void Disposing_Binding_Setter_Should_Preserve_LocalValue()
public void Styled_Property_Setter_With_TwoWay_Binding_Should_Update_Source()
{
var control = new Canvas();
var source = new { Foo = "foo" };
var setter = new Setter(TextBlock.TagProperty, new Binding
var data = new Data { Bar = Brushes.Red };
var control = new Border
{
Source = source,
Path = nameof(source.Foo),
});
DataContext = data,
};
var instance = setter.Instance(control);
instance.Start(true);
instance.Activate();
var style = new Style(x => x.OfType<Border>())
{
Setters =
{
new Setter
{
Property = Border.BackgroundProperty,
Value = new Binding
{
Path = "Bar",
Mode = BindingMode.TwoWay
}
}
},
};
Apply(style, control);
Assert.Equal(Brushes.Red, control.Background);
control.Background = Brushes.Green;
Assert.Equal(Brushes.Green, data.Bar);
}
private void Apply(Style style, Control control)
{
style.TryAttach(control, null);
}
control.Tag = "bar";
private void Apply(Setter setter, Control control)
{
var style = new Style(x => x.Is<Control>())
{
Setters = { setter },
};
instance.Dispose();
Apply(style, control);
}
Assert.Equal("bar", control.Tag);
private class Data
{
public string? Foo { get; set; }
public IBrush? Bar { get; set; }
}
private class TestConverter : IValueConverter

92
tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
@ -146,7 +147,7 @@ namespace Avalonia.Base.UnitTests.Styling
target.Classes.Add("foo");
target.Classes.Remove("foo");
Assert.Equal(new[] { "foodefault", "Foo", "Bar", "foodefault" }, values);
Assert.Equal(new[] { "foodefault", "Bar", "foodefault" }, values);
}
[Fact]
@ -463,39 +464,40 @@ namespace Avalonia.Base.UnitTests.Styling
[Fact]
public void Template_In_Inactive_Style_Is_Not_Built()
{
var instantiationCount = 0;
var template = new FuncTemplate<Class1>(() =>
{
++instantiationCount;
return new Class1();
});
Styles styles = new Styles
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
}
};
var target = new Class1();
target.BeginBatchUpdate();
styles.TryAttach(target, null);
target.EndBatchUpdate();
Assert.NotNull(target.Child);
Assert.Equal(1, instantiationCount);
throw new NotImplementedException();
////var instantiationCount = 0;
////var template = new FuncTemplate<Class1>(() =>
////{
//// ++instantiationCount;
//// return new Class1();
////});
////Styles styles = new Styles
////{
//// new Style(x => x.OfType<Class1>())
//// {
//// Setters =
//// {
//// new Setter(Class1.ChildProperty, template),
//// },
//// },
//// new Style(x => x.OfType<Class1>())
//// {
//// Setters =
//// {
//// new Setter(Class1.ChildProperty, template),
//// },
//// }
////};
////var target = new Class1();
////target.BeginBatchUpdate();
////styles.TryAttach(target, null);
////target.EndBatchUpdate();
////Assert.NotNull(target.Child);
////Assert.Equal(1, instantiationCount);
}
[Fact]
@ -702,6 +704,28 @@ namespace Avalonia.Base.UnitTests.Styling
}
}
[Fact]
public void DetachStyles_Should_Detach_Activator()
{
Style style = new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
};
var target = new Class1();
style.TryAttach(target, null);
Assert.Equal(1, target.Classes.ListenerCount);
((IStyleable)target).DetachStyles();
Assert.Equal(0, target.Classes.ListenerCount);
}
[Fact]
public void Should_Set_Owner_On_Assigned_Resources()
{

67
tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs

@ -6,6 +6,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
@ -35,20 +36,6 @@ namespace Avalonia.Base.UnitTests.Styling
Assert.Equal(parent, target.InheritanceParent);
}
[Fact]
public void Setting_Parent_Should_Not_Set_InheritanceParent_If_Already_Set()
{
var parent = new Decorator();
var inheritanceParent = new Decorator();
var target = new TestControl();
((ISetInheritanceParent)target).SetParent(inheritanceParent);
parent.Child = target;
Assert.Equal(parent, target.Parent);
Assert.Equal(inheritanceParent, target.InheritanceParent);
}
[Fact]
public void InheritanceParent_Should_Be_Cleared_When_Removed_From_Parent()
{
@ -61,20 +48,6 @@ namespace Avalonia.Base.UnitTests.Styling
Assert.Null(target.InheritanceParent);
}
[Fact]
public void InheritanceParent_Should_Be_Cleared_When_Removed_From_Parent_When_Has_Different_InheritanceParent()
{
var parent = new Decorator();
var inheritanceParent = new Decorator();
var target = new TestControl();
((ISetInheritanceParent)target).SetParent(inheritanceParent);
parent.Child = target;
parent.Child = null;
Assert.Null(target.InheritanceParent);
}
[Fact]
public void Adding_Element_With_Null_Parent_To_Logical_Tree_Should_Throw()
{
@ -126,7 +99,7 @@ namespace Avalonia.Base.UnitTests.Styling
Assert.True(childRaised);
Assert.True(grandchildRaised);
}
[Fact]
public void AttachedToLogicalTree_Should_Be_Called_Before_Parent_Change_Signalled()
{
@ -329,6 +302,8 @@ namespace Avalonia.Base.UnitTests.Styling
var root = new TestRoot();
var child = new Border();
AvaloniaLocator.CurrentMutable.BindToSelf<IStyler>(new Styler());
root.Child = child;
Assert.Throws<InvalidOperationException>(() => child.Name = "foo");
@ -351,22 +326,28 @@ namespace Avalonia.Base.UnitTests.Styling
}
[Fact]
public void StyleInstance_Is_Disposed_When_Control_Removed_From_Logical_Tree()
public void Style_Is_Removed_When_Control_Removed_From_Logical_Tree()
{
using (AvaloniaLocator.EnterScope())
var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = new Border();
var root = new TestRoot
{
var root = new TestRoot();
var child = new Border();
root.Child = child;
var styleInstance = new Mock<IStyleInstance>();
((IStyleable)child).StyleApplied(styleInstance.Object);
root.Child = null;
Styles =
{
new Style(x => x.OfType<Border>())
{
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Red),
}
}
},
Child = target,
};
styleInstance.Verify(x => x.Dispose(), Times.Once);
}
Assert.Equal(Brushes.Red, target.Background);
root.Child = null;
Assert.Null(target.Background);
}
[Fact]
@ -474,7 +455,7 @@ namespace Avalonia.Base.UnitTests.Styling
root.DataContext = "foo";
Assert.Equal(
new[]
new[]
{
"begin root",
"begin a1",

1
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -5,6 +5,7 @@
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
</PropertyGroup>
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />

147
tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs

@ -1,147 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Media;
using Avalonia.Styling;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Styling
{
[MemoryDiagnoser]
public class ControlTheme_Apply
{
private ControlTheme _theme;
private ControlTheme _otherTheme;
private List<Style> _styles = new();
public ControlTheme_Apply()
{
RuntimeHelpers.RunClassConstructor(typeof(TestControl).TypeHandle);
_theme = CreateControlTheme(Brushes.Red);
_otherTheme = CreateControlTheme(Brushes.Orange);
for (var i = 0; i < 100; ++i)
{
_styles.Add(new Style(x => x.OfType<TestControl>())
{
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Yellow) }
});
}
}
[Benchmark]
public void Apply_Control_Theme()
{
var target = new TestControl();
target.BeginBatchUpdate();
_theme.TryAttach(target, null);
target.ApplyTemplate();
_theme.TryAttach(target.VisualChild, null);
target.EndBatchUpdate();
}
[Benchmark]
public void Apply_Remove_Control_Theme()
{
var target = new TestControl();
target.BeginBatchUpdate();
_theme.TryAttach(target, null);
target.ApplyTemplate();
_theme.TryAttach(target.VisualChild, null);
target.EndBatchUpdate();
// Switching to another theme will cause the current theme to be removed but won't
// immediately apply the new theme, so for the benefit of the benchmark it has the
// effect of simply removing the theme.
target.Theme = _otherTheme;
}
[Benchmark]
public void Apply_Control_Theme_With_Styles()
{
var target = new TestControl();
target.BeginBatchUpdate();
_theme.TryAttach(target, null);
target.ApplyTemplate();
_theme.TryAttach(target.VisualChild, null);
foreach (var style in _styles)
style.TryAttach(target, null);
target.EndBatchUpdate();
}
[Benchmark]
public void Apply_Remove_Control_Theme_With_Styles()
{
var target = new TestControl();
target.BeginBatchUpdate();
_theme.TryAttach(target, null);
target.ApplyTemplate();
_theme.TryAttach(target.VisualChild, null);
foreach (var style in _styles)
style.TryAttach(target, null);
target.EndBatchUpdate();
// Switching to another theme will cause the current theme to be removed but won't
// immediately apply the new theme, so for the benefit of the benchmark it has the
// effect of simply removing the theme.
target.Theme = _otherTheme;
}
private static ControlTheme CreateControlTheme(IBrush background)
{
return new ControlTheme(typeof(TestControl))
{
Setters =
{
new Setter(TestControl.BackgroundProperty, Brushes.Transparent),
new Setter(TestControl.TemplateProperty, new FuncControlTemplate<TestControl>((_, x) =>
new Border())),
},
Children =
{
new Style(x => x.Nesting().Template().OfType<Border>())
{
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Red), }
},
new Style(x => x.Nesting().Class(":pointerover").Template().OfType<Border>())
{
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Green), }
},
new Style(x => x.Nesting().Class(":pressed").Template().OfType<Border>())
{
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Blue), }
},
}
};
}
private class TestControl : TemplatedControl
{
public IStyleable VisualChild => (IStyleable)VisualChildren[0];
}
private class TestClass2 : Control
{
}
}
}

7
tests/Avalonia.Benchmarks/Styling/Style_Apply.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Styling;
@ -52,12 +53,12 @@ namespace Avalonia.Benchmarks.Styling
{
var target = new TestClass();
target.BeginBatchUpdate();
target.GetValueStore().BeginStyling();
foreach (var style in _styles)
style.TryAttach(target, null);
target.EndBatchUpdate();
target.GetValueStore().EndStyling();
}
private class TestClass : Control

12
tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs

@ -32,12 +32,12 @@ namespace Avalonia.Benchmarks.Styling
{
var target = new TestClass();
target.BeginBatchUpdate();
target.GetValueStore().BeginStyling();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
target.GetValueStore().EndStyling();
}
[Benchmark(OperationsPerInvoke = 50)]
@ -45,12 +45,12 @@ namespace Avalonia.Benchmarks.Styling
{
var target = new TestClass();
target.BeginBatchUpdate();
target.GetValueStore().BeginStyling();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
target.GetValueStore().EndStyling();
target.Classes.Add("foo");
target.Classes.Remove("foo");
@ -61,12 +61,12 @@ namespace Avalonia.Benchmarks.Styling
{
var target = new TestClass();
target.BeginBatchUpdate();
target.GetValueStore().BeginStyling();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
target.GetValueStore().EndStyling();
target.DetachStyles();
}

21
tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs

@ -2,7 +2,6 @@ using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
@ -21,7 +20,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
var setter = (Setter)(style.Setters.First());
Assert.IsType<Binding>(setter.Value);
}
}
}
[Fact]
@ -39,17 +38,23 @@ namespace Avalonia.Markup.Xaml.UnitTests
DataContext = data,
};
var setter = new Setter
var style = new Style()
{
Property = TextBox.TextProperty,
Value = new Binding
Setters =
{
Path = "Foo",
Mode = BindingMode.TwoWay
new Setter
{
Property = TextBox.TextProperty,
Value = new Binding
{
Path = "Foo",
Mode = BindingMode.TwoWay
}
}
}
};
setter.Instance(control).Start(false);
style.TryAttach(control, control);
Assert.Equal("foo", control.Text);
control.Text = "bar";

2
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -453,7 +453,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
collection.Remove(Brushes.Green);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors().ToList());
collection.Add(Brushes.Violet);
collection.Add(Brushes.Black);

Loading…
Cancel
Save