diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index b045a32cd1..edaa76233e 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/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) { diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs index 52cd4b324f..0881fde988 100644 --- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs +++ b/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)!; diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index d8fcce803f..29e0b0a7c9 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -28,6 +28,7 @@ + @@ -39,6 +40,7 @@ + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 6633eabb5d..68c8f19f19 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Runtime.CompilerServices; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.PropertyStore; -using Avalonia.Reactive; using Avalonia.Threading; namespace Avalonia @@ -18,13 +18,11 @@ namespace Avalonia /// public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { + private readonly ValueStore _values; private AvaloniaObject? _inheritanceParent; - private List? _directBindings; private PropertyChangedEventHandler? _inpcChanged; private EventHandler? _propertyChanged; private List? _inheritanceChildren; - private ValueStore? _values; - private bool _batchUpdate; /// /// Initializes a new instance of the class. @@ -32,6 +30,7 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); + _values = new ValueStore(this); } /// @@ -59,7 +58,7 @@ namespace Avalonia /// /// The inheritance parent. /// - protected AvaloniaObject? InheritanceParent + protected internal AvaloniaObject? InheritanceParent { get { @@ -72,28 +71,10 @@ namespace Avalonia if (_inheritanceParent != value) { - var oldParent = _inheritanceParent; - var valuestore = _values; - _inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent = value; - - var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()); - var propertiesCount = properties.Count; - - for (var i = 0; i < propertiesCount; i++) - { - var property = properties[i]; - if (valuestore?.IsSet(property) == true) - { - // If local value set there can be no change. - continue; - } - - property.RouteInheritanceParentChanged(this, oldParent); - } - _inheritanceParent?.AddInheritanceChild(this); + _values.SetInheritanceParent(value); } } } @@ -118,24 +99,15 @@ namespace Avalonia set { this.Bind(binding.Property!, value); } } - private ValueStore Values - { - get - { - if (_values is null) - { - _values = new ValueStore(this); - - if (_batchUpdate) - _values.BeginBatchUpdate(); - } - - return _values; - } - } - + /// + /// Returns a value indicating whether the current thread is the UI thread. + /// + /// true if the current thread is the UI thread; otherwise false. public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); + /// + /// Checks that the current thread is the UI thread and throws if not. + /// public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); /// @@ -144,9 +116,9 @@ namespace Avalonia /// The property. 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); } /// @@ -234,9 +206,12 @@ namespace Avalonia /// The value. public object? GetValue(AvaloniaProperty property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + _ = property ?? throw new ArgumentNullException(nameof(property)); - return property.RouteGetValue(this); + if (property.IsDirect) + return property.RouteGetValue(this); + else + return _values.GetValue(property); } /// @@ -247,10 +222,9 @@ namespace Avalonia /// The value. public T GetValue(StyledPropertyBase property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - - return GetValueOrInheritedOrDefault(property); + return _values.GetValue(property); } /// @@ -269,18 +243,11 @@ namespace Avalonia } /// - public Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority) + public Optional GetBaseValue(StyledPropertyBase property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - - if (_values is object && - _values.TryGetValue(property, maxPriority, out var value)) - { - return value; - } - - return default; + return _values.GetBaseValue(property); } /// @@ -346,26 +313,20 @@ namespace Avalonia T value, BindingPriority priority = BindingPriority.LocalValue) { - property = property ?? throw new ArgumentNullException(nameof(property)); + _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); + ValidatePriority(priority); - LogPropertySet(property, value, priority); + LogPropertySet(property, value, BindingPriority.LocalValue); if (value is UnsetValueType) { if (priority == BindingPriority.LocalValue) - { - Values.ClearLocalValue(property); - } - else - { - throw new NotSupportedException( - "Cannot set property to Unset at non-local value priority."); - } + _values.ClearLocalValue(property); } - else if (!(value is DoNothingType)) + else if (value is not DoNothingType) { - return Values.SetValue(property, value, priority); + return _values.SetValue(property, value, priority); } return null; @@ -382,6 +343,7 @@ namespace Avalonia property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); + property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); LogPropertySet(property, value, BindingPriority.LocalValue); SetDirectValueUnchecked(property, value); } @@ -398,12 +360,52 @@ namespace Avalonia public IDisposable Bind( AvaloniaProperty property, IObservable source, + BindingPriority priority = BindingPriority.LocalValue) => property.RouteBind(this, source, priority); + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public IDisposable Bind( + StyledPropertyBase property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + VerifyAccess(); + ValidatePriority(priority); + + return _values.AddBinding(property, source, priority); + } + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public IDisposable Bind( + StyledPropertyBase property, + IObservable source, BindingPriority priority = BindingPriority.LocalValue) { property = property ?? throw new ArgumentNullException(nameof(property)); source = source ?? throw new ArgumentNullException(nameof(source)); + VerifyAccess(); + ValidatePriority(priority); - return property.RouteBind(this, source.ToBindingValue(), priority); + return _values.AddBinding(property, source, priority); } /// @@ -424,8 +426,9 @@ namespace Avalonia property = property ?? throw new ArgumentNullException(nameof(property)); source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); + ValidatePriority(priority); - return Values.AddBinding(property, source, priority); + return _values.AddBinding(property, source, priority); } /// @@ -439,10 +442,9 @@ namespace Avalonia /// public IDisposable Bind( DirectPropertyBase property, - IObservable> source) + IObservable source) { property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); @@ -452,48 +454,67 @@ namespace Avalonia throw new ArgumentException($"The property {property.Name} is readonly."); } - Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( - this, - "Bound {Property} to {Binding} with priority LocalValue", - property, - GetDescription(source)); - - _directBindings ??= new List(); - - return new DirectBindingSubscription(this, property, source); + return _values.AddBinding(property, source); } /// - /// Coerces the specified . + /// Binds a to an observable. /// + /// The type of the property. /// The property. - public void CoerceValue(AvaloniaProperty property) + /// The observable. + /// + /// A disposable which can be used to terminate the binding. + /// + public IDisposable Bind( + DirectPropertyBase property, + IObservable source) { - _values?.CoerceValue(property); - } + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); - public void BeginBatchUpdate() - { - if (_batchUpdate) + property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + + if (property.IsReadOnly) { - throw new InvalidOperationException("Batch update already in progress."); + throw new ArgumentException($"The property {property.Name} is readonly."); } - _batchUpdate = true; - _values?.BeginBatchUpdate(); + return _values.AddBinding(property, source); } - public void EndBatchUpdate() + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + /// + /// A disposable which can be used to terminate the binding. + /// + public IDisposable Bind( + DirectPropertyBase property, + IObservable> source) { - if (!_batchUpdate) + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + + if (property.IsReadOnly) { - throw new InvalidOperationException("No batch update in progress."); + throw new ArgumentException($"The property {property.Name} is readonly."); } - _batchUpdate = false; - _values?.EndBatchUpdate(); + return _values.AddBinding(property, source); } + /// + /// Coerces the specified . + /// + /// The property. + public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property); + /// internal void AddInheritanceChild(AvaloniaObject child) { @@ -507,98 +528,12 @@ namespace Avalonia _inheritanceChildren?.Remove(child); } - internal void InheritedPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - Optional newValue) - { - if (property.Inherits && (_values == null || !_values.IsSet(property))) - { - RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); - } - } - /// Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers() { return _propertyChanged?.GetInvocationList(); } - internal void ValueChanged(AvaloniaPropertyChangedEventArgs change) - { - var property = (StyledPropertyBase)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(property)); - } - - if (change.IsEffectiveValueChange && !change.NewValue.HasValue) - { - change.SetNewValue(GetInheritedOrDefault(property)); - } - - if (!change.IsEffectiveValueChange || - !EqualityComparer.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( - StyledPropertyBase property, - IPriorityValueEntry entry, - Optional oldValue) - { - var change = new AvaloniaPropertyChangedEventArgs( - this, - property, - oldValue, - default, - BindingPriority.Unset); - ValueChanged(change); - } - - /// - /// Called for each inherited property when the changes. - /// - /// The type of the property value. - /// The property. - /// The old inheritance parent. - internal void InheritanceParentChanged( - StyledPropertyBase property, - AvaloniaObject? oldParent) - { - var oldValue = oldParent is not null ? - oldParent.GetValueOrInheritedOrDefault(property) : - property.GetDefaultValue(GetType()); - - var newValue = GetInheritedOrDefault(property); - - if (!EqualityComparer.Default.Equals(oldValue, newValue)) - { - RaisePropertyChanged(property, oldValue, newValue); - } - } - internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property) { if (property.IsDirect) @@ -626,19 +561,23 @@ namespace Avalonia "Unset"); } + internal ValueStore GetValueStore() => _values; + internal IReadOnlyList? GetInheritanceChildren() => _inheritanceChildren; + /// - /// Logs a binding error for a property. + /// Gets a logger to which a binding warning may be written. /// /// The property that the error occurred on. - /// The binding error. - protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e) + /// The binding exception, if any. + /// + /// This is overridden in to prevent logging binding errors when a + /// control is not attached to the visual tree. + /// + 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); } /// @@ -675,6 +614,22 @@ namespace Avalonia { } + /// + /// Raises the event for a direct property. + /// + /// The property that has changed. + /// The old property value. + /// The new property value. + /// The priority of the binding that produced the value. + private protected void RaisePropertyChanged( + DirectPropertyBase property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority = BindingPriority.LocalValue) + { + RaisePropertyChanged(property, oldValue, newValue, priority, true); + } + /// /// Raises the event. /// @@ -682,18 +637,32 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected internal void RaisePropertyChanged( + /// + /// Whether the notification represents a change to the effective value of the property. + /// + internal void RaisePropertyChanged( AvaloniaProperty property, Optional oldValue, BindingValue newValue, - BindingPriority priority = BindingPriority.LocalValue) + BindingPriority priority, + bool isEffectiveValue) { - RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs( + var e = new AvaloniaPropertyChangedEventArgs( this, property, oldValue, newValue, - priority)); + priority, + isEffectiveValue); + + OnPropertyChangedCore(e); + + if (isEffectiveValue) + { + property.NotifyChanged(e); + _propertyChanged?.Invoke(this, e); + _inpcChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name)); + } } /// @@ -718,110 +687,24 @@ namespace Avalonia var old = field; field = value; - RaisePropertyChanged(property, old, value); + RaisePropertyChanged(property, old, value, BindingPriority.LocalValue, true); return true; } - private T GetInheritedOrDefault(StyledPropertyBase property) - { - if (property.Inherits && InheritanceParent is AvaloniaObject o) - { - return o.GetValueOrInheritedOrDefault(property); - } - - return property.GetDefaultValue(GetType()); - } - - private T GetValueOrInheritedOrDefault( - StyledPropertyBase 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(AvaloniaPropertyChangedEventArgs 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); - } - } - } - /// /// Sets the value of a direct property. /// /// The property. /// The value. - private void SetDirectValueUnchecked(DirectPropertyBase property, T value) + internal void SetDirectValueUnchecked(DirectPropertyBase property, T value) { - var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); - if (value is UnsetValueType) { - p.InvokeSetter(this, p.GetUnsetValue(GetType())); + property.InvokeSetter(this, property.GetUnsetValue(GetType())); } else if (!(value is DoNothingType)) { - p.InvokeSetter(this, value); + property.InvokeSetter(this, value); } } @@ -830,16 +713,9 @@ namespace Avalonia /// /// The property. /// The value. - private void SetDirectValueUnchecked(DirectPropertyBase property, BindingValue value) + internal void SetDirectValueUnchecked(DirectPropertyBase property, BindingValue value) { - var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property); - - if (p == null) - { - throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); - } - - LogIfError(property, value); + LoggingUtils.LogIfNecessary(this, property, value); switch (value.Type) { @@ -858,7 +734,7 @@ namespace Avalonia break; } - var metadata = p.GetMetadata(GetType()); + var metadata = property.GetMetadata(GetType()); if (metadata.EnableDataValidation == true) { @@ -877,29 +753,6 @@ namespace Avalonia return description?.Description ?? o.ToString() ?? o.GetType().Name; } - /// - /// Logs a message if the notification represents a binding error. - /// - /// The property being bound. - /// The binding notification. - private void LogIfError(AvaloniaProperty property, BindingValue value) - { - if (value.HasError) - { - if (value.Error is AggregateException aggregate) - { - foreach (var inner in aggregate.InnerExceptions) - { - LogBindingError(property, inner); - } - } - else - { - LogBindingError(property, value.Error!); - } - } - } - /// /// Logs a property set message. /// @@ -916,49 +769,16 @@ namespace Avalonia priority); } - private class DirectBindingSubscription : IObserver>, IDisposable + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ValidatePriority(BindingPriority priority) { - private readonly AvaloniaObject _owner; - private readonly DirectPropertyBase _property; - private readonly IDisposable _subscription; - - public DirectBindingSubscription( - AvaloniaObject owner, - DirectPropertyBase property, - IObservable> source) - { - _owner = owner; - _property = property; - _owner._directBindings!.Add(this); - _subscription = source.Subscribe(this); - } - - public void Dispose() - { - // _subscription can be null, if Subscribe failed with an exception. - _subscription?.Dispose(); - _owner._directBindings!.Remove(this); - } - - public void OnCompleted() => Dispose(); - public void OnError(Exception error) => Dispose(); - public void OnNext(BindingValue value) - { - if (Dispatcher.UIThread.CheckAccess()) - { - _owner.SetDirectValueUnchecked(_property, value); - } - else - { - // To avoid allocating closure in the outer scope we need to capture variables - // locally. This allows us to skip most of the allocations when on UI thread. - var instance = _owner; - var property = _property; - var newValue = value; + if (priority < BindingPriority.Animation || priority >= BindingPriority.Inherited) + ThrowInvalidPriority(priority); + } - Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue)); - } - } + private static void ThrowInvalidPriority(BindingPriority priority) + { + throw new ArgumentException($"Invalid priority ${priority}", nameof(priority)); } } } diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 2d7bab6cd6..9644063da7 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -261,7 +261,6 @@ namespace Avalonia } throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); - } /// @@ -280,14 +279,17 @@ namespace Avalonia IObservable source, BindingPriority priority = BindingPriority.LocalValue) { - target = target ?? throw new ArgumentNullException(nameof(target)); - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); + if (target is AvaloniaObject ao) + { + return property switch + { + StyledPropertyBase styled => ao.Bind(styled, source, priority), + DirectPropertyBase direct => ao.Bind(direct, source), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), + }; + } - return target.Bind( - property, - source.ToBindingValue(), - priority); + throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); } /// @@ -362,10 +364,8 @@ namespace Avalonia /// /// The object. /// The property. - /// The maximum priority for the value. /// - /// For styled properties, gets the value of the property if set on the object with a - /// priority equal or lower to , otherwise + /// For styled properties, gets the value of the property excluding animated values, otherwise /// . Note that this method does not return /// property values that come from inherited or default values. /// @@ -373,14 +373,13 @@ namespace Avalonia /// public static object? GetBaseValue( this IAvaloniaObject target, - AvaloniaProperty property, - BindingPriority maxPriority) + AvaloniaProperty property) { target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); if (target is AvaloniaObject ao) - return property.RouteGetBaseValue(ao, maxPriority); + return property.RouteGetBaseValue(ao); throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); } @@ -389,10 +388,8 @@ namespace Avalonia /// /// The object. /// The property. - /// The maximum priority for the value. /// - /// For styled properties, gets the value of the property if set on the object with a - /// priority equal or lower to , otherwise + /// For styled properties, gets the value of the property excluding animated values, otherwise /// . Note that this method does not return property values /// that come from inherited or default values. /// @@ -400,8 +397,7 @@ namespace Avalonia /// public static Optional GetBaseValue( this IAvaloniaObject target, - AvaloniaProperty property, - BindingPriority maxPriority) + AvaloniaProperty property) { target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); @@ -410,7 +406,7 @@ namespace Avalonia { return property switch { - StyledPropertyBase styled => ao.GetBaseValue(styled, maxPriority), + StyledPropertyBase styled => ao.GetBaseValue(styled), DirectPropertyBase direct => ao.GetValue(direct), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") }; diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 46ba4082fb..59a61e3424 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/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; } + /// + /// Creates an effective value for the property. + /// + /// The effective value owner. + internal abstract EffectiveValue CreateEffectiveValue(AvaloniaObject o); + /// /// Routes an untyped ClearValue call to a typed call. /// @@ -471,8 +478,7 @@ namespace Avalonia /// Routes an untyped GetBaseValue call to a typed call. /// /// The object instance. - /// The maximum priority for the value. - internal abstract object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority); + internal abstract object? RouteGetBaseValue(AvaloniaObject o); /// /// Routes an untyped SetValue call to a typed call. @@ -496,12 +502,9 @@ namespace Avalonia /// The priority. internal abstract IDisposable RouteBind( AvaloniaObject o, - IObservable> source, + IObservable source, BindingPriority priority); - internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent); - internal abstract ISetterInstance CreateSetterInstance(IStyleable target, object? value); - /// /// Overrides the metadata for the property on the specified type. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index 45c67b9f48..a3ca25bc45 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/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; + } + /// /// Gets the that the property changed on. /// @@ -49,20 +59,8 @@ namespace Avalonia /// public BindingPriority Priority { get; private set; } - /// - /// Gets a value indicating whether the change represents a change to the effective value of - /// the property. - /// - /// - /// This will usually be true, except in - /// - /// 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. - /// - 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(); diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs index 734e38596c..2c7a597537 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs @@ -21,7 +21,18 @@ namespace Avalonia Optional oldValue, BindingValue newValue, BindingPriority priority) - : base(sender, priority) + : this(sender, property, oldValue, newValue, priority, true) + { + } + + internal AvaloniaPropertyChangedEventArgs( + IAvaloniaObject sender, + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority, + bool isEffectiveValueChange) + : base(sender, priority, isEffectiveValueChange) { Property = property; OldValue = oldValue; @@ -39,28 +50,13 @@ namespace Avalonia /// /// Gets the old value of the property. /// - /// - /// When is true, returns the - /// old value of the property on the object. - /// When is false, returns - /// . - /// public new Optional OldValue { get; private set; } /// /// Gets the new value of the property. /// - /// - /// When is true, returns the - /// value of the property on the object. - /// When is false returns the - /// changed value, or if the value was removed. - /// public new BindingValue NewValue { get; private set; } - internal void SetOldValue(Optional value) => OldValue = value; - internal void SetNewValue(BindingValue value) => NewValue = value; - protected override AvaloniaProperty GetProperty() => Property; protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs b/src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs new file mode 100644 index 0000000000..e22288a74d --- /dev/null +++ b/src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace System; + +#if !NET6_0_OR_GREATER +internal static class CollectionCompatibilityExtensions +{ + public static bool Remove( + this Dictionary 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(this Dictionary o, TKey key, TValue value) + where TKey : notnull + { + if (!o.ContainsKey(key)) + { + o.Add(key, value); + return true; + } + + return false; + } +} +#endif diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index 4b97b1a02c..b67056e554 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -241,26 +241,6 @@ namespace Avalonia.Data _value = value; } - public BindingValue ToBindingValue() - { - if (ErrorType == BindingErrorType.None) - { - return HasValue ? new BindingValue(Value) : BindingValue.Unset; - } - else if (ErrorType == BindingErrorType.Error) - { - return BindingValue.BindingError( - Error!, - HasValue ? new Optional(Value) : Optional.Empty); - } - else - { - return BindingValue.DataValidationError( - Error!, - HasValue ? new Optional(Value) : Optional.Empty); - } - } - /// public override string ToString() { diff --git a/src/Avalonia.Base/Data/BindingPriority.cs b/src/Avalonia.Base/Data/BindingPriority.cs index ece64375f2..dd1654f53c 100644 --- a/src/Avalonia.Base/Data/BindingPriority.cs +++ b/src/Avalonia.Base/Data/BindingPriority.cs @@ -35,6 +35,11 @@ namespace Avalonia.Data /// A style binding. /// Style, + + /// + /// The value is inherited from an ancestor element. + /// + Inherited, /// /// The binding is uninitialized. diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 55be611083..4bb3ad08d5 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using Avalonia.Utilities; @@ -230,19 +231,77 @@ namespace Avalonia.Data /// /// Creates a from an object, handling the special values - /// and . + /// , and + /// . /// /// The untyped value. /// The typed binding value. public static BindingValue FromUntyped(object? value) { - return value switch + return FromUntyped(value, typeof(T)); + } + + /// + /// Creates a from an object, handling the special values + /// , and + /// . + /// + /// The untyped value. + /// The runtime target type. + /// The typed binding value. + public static BindingValue FromUntyped(object? value, Type targetType) + { + if (value == AvaloniaProperty.UnsetValue) + return Unset; + else if (value == BindingOperations.DoNothing) + return DoNothing; + + var type = BindingValueType.Value; + T? v = default; + Exception? error = null; + List? errors = null; + + if (value is BindingNotification n) { - UnsetValueType _ => Unset, - DoNothingType _ => DoNothing, - BindingNotification n => n.ToBindingValue().Cast(), - _ => new BindingValue((T)value!) - }; + error = n.Error; + type = n.ErrorType switch + { + BindingErrorType.Error => BindingValueType.BindingError, + BindingErrorType.DataValidationError => BindingValueType.DataValidationError, + _ => BindingValueType.Value, + }; + + if (n.HasValue) + type |= BindingValueType.HasValue; + value = n.Value; + } + + if ((type & BindingValueType.HasValue) != 0) + { + if (TypeUtilities.TryConvertImplicit(targetType, value, out var typed)) + v = (T)typed!; + else + { + var e = new InvalidCastException( + $"Unable to convert object '{value ?? "(null)"}' " + + $"of type '{value?.GetType()}' to type '{targetType}'."); + + if (error is null) + error = e; + else + { + errors ??= new List() { error }; + errors.Add(e); + } + + type = BindingValueType.BindingError; + } + } + + if (errors is not null) + error = new AggregateException(errors); + + return new BindingValue(type, v, error); } /// @@ -372,61 +431,4 @@ namespace Avalonia.Data } } } - - public static class BindingValueExtensions - { - /// - /// Casts the type of a using only the C# cast operator. - /// - /// The target type. - /// The binding value. - /// The cast value. - public static BindingValue Cast(this BindingValue value) - { - return value.Type switch - { - BindingValueType.DoNothing => BindingValue.DoNothing, - BindingValueType.UnsetValue => BindingValue.Unset, - BindingValueType.Value => new BindingValue((T)value.Value!), - BindingValueType.BindingError => BindingValue.BindingError(value.Error!), - BindingValueType.BindingErrorWithFallback => BindingValue.BindingError( - value.Error!, - (T)value.Value!), - BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!), - BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError( - value.Error!, - (T)value.Value!), - _ => throw new NotSupportedException("Invalid BindingValue type."), - }; - } - - /// - /// Casts the type of a using the implicit conversions - /// allowed by the C# language. - /// - /// The target type. - /// The binding value. - /// The cast value. - /// - /// Note that this method uses reflection and as such may be slow. - /// - public static BindingValue Convert(this BindingValue value) - { - return value.Type switch - { - BindingValueType.DoNothing => BindingValue.DoNothing, - BindingValueType.UnsetValue => BindingValue.Unset, - BindingValueType.Value => new BindingValue(TypeUtilities.ConvertImplicit(value.Value!)), - BindingValueType.BindingError => BindingValue.BindingError(value.Error!), - BindingValueType.BindingErrorWithFallback => BindingValue.BindingError( - value.Error!, - TypeUtilities.ConvertImplicit(value.Value!)), - BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!), - BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError( - value.Error!, - TypeUtilities.ConvertImplicit(value.Value!)), - _ => throw new NotSupportedException("Invalid BindingValue type."), - }; - } - } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index e4affd97de..0c7f576da6 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -52,6 +52,7 @@ namespace Avalonia.Data.Core private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; private object? _root; + private Func? _rootGetter; private IDisposable? _rootSubscription; private WeakReference? _value; private IReadOnlyList? _transformNodes; @@ -109,11 +110,9 @@ namespace Avalonia.Data.Core IObservable update, string? description) { - _ = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter)); - Description = description; - _node = node ?? throw new ArgumentNullException(nameof(rootGetter)); - _node.Target = new WeakReference(rootGetter()); + _rootGetter = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter)); + _node = node ?? throw new ArgumentNullException(nameof(node)); _root = update.Select(x => rootGetter()); } @@ -263,6 +262,8 @@ namespace Avalonia.Data.Core protected override void Initialize() { _value = null; + if (_rootGetter is not null) + _node.Target = new WeakReference(_rootGetter()); _node.Subscribe(ValueChanged); StartRoot(); } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 86e4bffaa8..0a8fe19885 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Reactive; using Avalonia.Styling; @@ -105,6 +106,11 @@ namespace Avalonia base.OverrideMetadata(type, metadata); } + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) + { + throw new InvalidOperationException("Cannot create EffectiveValue for direct property."); + } + /// internal override void RouteClearValue(AvaloniaObject o) { @@ -117,7 +123,7 @@ namespace Avalonia return o.GetValue(this); } - internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(AvaloniaObject o) { return o.GetValue(this); } @@ -146,44 +152,18 @@ namespace Avalonia return null; } - /// + /// + /// Routes an untyped Bind call to a typed call. + /// + /// The object instance. + /// The binding source. + /// The priority. internal override IDisposable RouteBind( AvaloniaObject o, - IObservable> source, + IObservable source, BindingPriority priority) { - var adapter = TypedBindingAdapter.Create(o, this, source); - return o.Bind(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( - target, - this, - binding); - } - else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType)) - { - return new PropertySetterTemplateInstance( - target, - this, - template); - } - else - { - return new PropertySetterInstance( - target, - this, - (TValue)value!); - } + return o.Bind(this, source); } } } diff --git a/src/Avalonia.Base/IStyledPropertyAccessor.cs b/src/Avalonia.Base/IStyledPropertyAccessor.cs index c4a0005f55..4cbfd7b759 100644 --- a/src/Avalonia.Base/IStyledPropertyAccessor.cs +++ b/src/Avalonia.Base/IStyledPropertyAccessor.cs @@ -15,5 +15,12 @@ namespace Avalonia /// The default value. /// object? GetDefaultValue(Type type); + + /// + /// Validates the specified property value. + /// + /// The value. + /// True if the value is valid, otherwise false. + bool ValidateValue(object? value); } } diff --git a/src/Avalonia.Base/PropertyStore/AvaloniaPropertyDictionaryPool.cs b/src/Avalonia.Base/PropertyStore/AvaloniaPropertyDictionaryPool.cs new file mode 100644 index 0000000000..5a57bc3786 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/AvaloniaPropertyDictionaryPool.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Avalonia.Utilities; + +namespace Avalonia.PropertyStore +{ + internal static class AvaloniaPropertyDictionaryPool + { + private const int MaxPoolSize = 4; + private static readonly Stack> _pool = new(); + + public static AvaloniaPropertyDictionary Get() + { + return _pool.Count == 0 ? new() : _pool.Pop(); + } + + public static void Release(AvaloniaPropertyDictionary dictionary) + { + if (_pool.Count < MaxPoolSize) + { + dictionary.Clear(); + _pool.Push(dictionary); + } + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs deleted file mode 100644 index 9a25e98a23..0000000000 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using Avalonia.Data; -using Avalonia.Threading; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents an untyped interface to . - /// - internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable - { - void Start(bool ignoreBatchUpdate); - } - - /// - /// Stores a binding in a or . - /// - /// The property type. - internal class BindingEntry : IBindingEntry, IPriorityValueEntry, IObserver> - { - private readonly AvaloniaObject _owner; - private ValueOwner _sink; - private IDisposable? _subscription; - private bool _isSubscribed; - private bool _batchUpdate; - private Optional _value; - - public BindingEntry( - AvaloniaObject owner, - StyledPropertyBase property, - IObservable> source, - BindingPriority priority, - ValueOwner sink) - { - _owner = owner; - Property = property; - Source = source; - Priority = priority; - _sink = sink; - } - - public StyledPropertyBase Property { get; } - public BindingPriority Priority { get; private set; } - public IObservable> Source { get; } - Optional IValue.GetValue() => _value.ToObject(); - - public void BeginBatchUpdate() => _batchUpdate = true; - - public void EndBatchUpdate() - { - _batchUpdate = false; - - if (_sink.IsValueStore) - Start(); - } - - public Optional GetValue(BindingPriority maxPriority) - { - return Priority >= maxPriority ? _value : Optional.Empty; - } - - public void Dispose() - { - _subscription?.Dispose(); - _subscription = null; - OnCompleted(); - } - - public void OnCompleted() - { - var oldValue = _value; - _value = default; - Priority = BindingPriority.Unset; - _isSubscribed = false; - _sink.Completed(Property, this, oldValue); - } - - public void OnError(Exception error) - { - throw new NotImplementedException("BindingEntry.OnError is not implemented", error); - } - - public void OnNext(BindingValue value) - { - if (Dispatcher.UIThread.CheckAccess()) - { - UpdateValue(value); - } - else - { - // To avoid allocating closure in the outer scope we need to capture variables - // locally. This allows us to skip most of the allocations when on UI thread. - var instance = this; - var newValue = value; - - Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue)); - } - } - - public void Start() => Start(false); - - public void Start(bool ignoreBatchUpdate) - { - // We can't use _subscription to check whether we're subscribed because it won't be set - // until Subscribe has finished, which will be too late to prevent reentrancy. In addition - // don't re-subscribe to completed/disposed bindings (indicated by Unset priority). - if (!_isSubscribed && - Priority != BindingPriority.Unset && - (!_batchUpdate || ignoreBatchUpdate)) - { - _isSubscribed = true; - _subscription = Source.Subscribe(this); - } - } - - public void Reparent(PriorityValue parent) => _sink = new(parent); - - public void RaiseValueChanged( - AvaloniaObject owner, - AvaloniaProperty property, - Optional oldValue, - Optional newValue) - { - owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - owner, - (AvaloniaProperty)property, - oldValue.Cast(), - newValue.Cast(), - Priority)); - } - - private void UpdateValue(BindingValue value) - { - if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) - { - value = Property.GetDefaultValue(_owner.GetType()); - } - - if (value.Type == BindingValueType.DoNothing) - { - return; - } - - var old = _value; - - if (value.Type != BindingValueType.DataValidationError) - { - _value = value.ToOptional(); - } - - _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(_owner, Property, old, value, Priority)); - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs new file mode 100644 index 0000000000..ef14211902 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + internal abstract class BindingEntryBase : IValueEntry, + IObserver, + IObserver>, + IDisposable + { + private static readonly IDisposable s_creating = Disposable.Empty; + private static readonly IDisposable s_creatingQuiet = Disposable.Create(() => { }); + private IDisposable? _subscription; + private bool _hasValue; + private TValue? _value; + + protected BindingEntryBase( + ValueFrame frame, + AvaloniaProperty property, + IObservable> source) + { + Frame = frame; + Source = source; + Property = property; + } + + protected BindingEntryBase( + ValueFrame frame, + AvaloniaProperty property, + IObservable source) + { + Frame = frame; + Source = source; + Property = property; + } + + public bool HasValue + { + get + { + Start(produceValue: false); + return _hasValue; + } + } + + public bool IsSubscribed => _subscription is not null; + public AvaloniaProperty Property { get; } + AvaloniaProperty IValueEntry.Property => Property; + protected ValueFrame Frame { get; } + protected object Source { get; } + + public void Dispose() + { + Unsubscribe(); + BindingCompleted(); + } + + public TValue GetValue() + { + Start(produceValue: false); + if (!_hasValue) + throw new AvaloniaInternalException("The binding entry has no value."); + return _value!; + } + + public void Start() => Start(true); + + public void OnCompleted() => BindingCompleted(); + public void OnError(Exception error) => BindingCompleted(); + public void OnNext(TSource value) => SetValue(ConvertAndValidate(value)); + public void OnNext(BindingValue value) => SetValue(ConvertAndValidate(value)); + + public virtual void Unsubscribe() + { + _subscription?.Dispose(); + _subscription = null; + } + + object? IValueEntry.GetValue() + { + Start(produceValue: false); + if (!_hasValue) + throw new AvaloniaInternalException("The BindingEntry has no value."); + return _value!; + } + + protected abstract BindingValue ConvertAndValidate(TSource value); + protected abstract BindingValue ConvertAndValidate(BindingValue value); + + protected virtual void Start(bool produceValue) + { + if (_subscription is not null) + return; + + _subscription = produceValue ? s_creating : s_creatingQuiet; + _subscription = Source switch + { + IObservable> bv => bv.Subscribe(this), + IObservable b => b.Subscribe(this), + _ => throw new AvaloniaInternalException("Unexpected binding source."), + }; + } + + private void ClearValue() + { + if (_hasValue) + { + _hasValue = false; + _value = default; + if (_subscription is not null) + Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); + } + } + + private void SetValue(BindingValue value) + { + if (Frame.Owner is null) + return; + + LoggingUtils.LogIfNecessary(Frame.Owner.Owner, Property, value); + + if (value.HasValue) + { + if (!_hasValue || !EqualityComparer.Default.Equals(_value, value.Value)) + { + _value = value.Value; + _hasValue = true; + if (_subscription is not null && _subscription != s_creatingQuiet) + Frame.Owner?.OnBindingValueChanged(this, Frame.Priority); + } + } + else if (value.Type != BindingValueType.DoNothing) + { + ClearValue(); + if (_subscription is not null && _subscription != s_creatingQuiet) + Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); + } + } + + private void BindingCompleted() + { + _subscription = null; + Frame.OnBindingCompleted(this); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs deleted file mode 100644 index 4116f4abd9..0000000000 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents an untyped interface to . - /// - internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable - { - } - - /// - /// Stores a value with a priority in a or - /// . - /// - /// The property type. - internal class ConstantValueEntry : IPriorityValueEntry, IConstantValueEntry - { - private ValueOwner _sink; - private Optional _value; - - public ConstantValueEntry( - StyledPropertyBase property, - T value, - BindingPriority priority, - ValueOwner sink) - { - Property = property; - _value = value; - Priority = priority; - _sink = sink; - } - - public ConstantValueEntry( - StyledPropertyBase property, - Optional value, - BindingPriority priority, - ValueOwner sink) - { - Property = property; - _value = value; - Priority = priority; - _sink = sink; - } - - public StyledPropertyBase Property { get; } - public BindingPriority Priority { get; private set; } - Optional IValue.GetValue() => _value.ToObject(); - - public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) - { - return Priority >= maxPriority ? _value : Optional.Empty; - } - - public void Dispose() - { - var oldValue = _value; - _value = default; - Priority = BindingPriority.Unset; - _sink.Completed(Property, this, oldValue); - } - - public void Reparent(PriorityValue sink) => _sink = new(sink); - public void Start() { } - - public void RaiseValueChanged( - AvaloniaObject owner, - AvaloniaProperty property, - Optional oldValue, - Optional newValue) - { - owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - owner, - (AvaloniaProperty)property, - oldValue.Cast(), - newValue.Cast(), - Priority)); - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs new file mode 100644 index 0000000000..cbe2435953 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs @@ -0,0 +1,76 @@ +using System; +using Avalonia.Data; +using Avalonia.Threading; + +namespace Avalonia.PropertyStore +{ + internal class DirectBindingObserver : IObserver, + IObserver>, + IDisposable + { + private readonly ValueStore _owner; + private IDisposable? _subscription; + + public DirectBindingObserver(ValueStore owner, DirectPropertyBase property) + { + _owner = owner; + Property = property; + } + + public DirectPropertyBase Property { get;} + + public void Start(IObservable source) + { + _subscription = source.Subscribe(this); + } + + public void Start(IObservable> source) + { + _subscription = source.Subscribe(this); + } + + public void Dispose() + { + _subscription?.Dispose(); + _subscription = null; + _owner.OnLocalValueBindingCompleted(Property, this); + } + + public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); + public void OnError(Exception error) => OnCompleted(); + + public void OnNext(T value) + { + if (Dispatcher.UIThread.CheckAccess()) + { + _owner.Owner.SetDirectValueUnchecked(Property, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = _owner.Owner; + var property = Property; + var newValue = value; + Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue)); + } + } + + public void OnNext(BindingValue value) + { + if (Dispatcher.UIThread.CheckAccess()) + { + _owner.Owner.SetDirectValueUnchecked(Property, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = _owner.Owner; + var property = Property; + var newValue = value; + Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue)); + } + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs new file mode 100644 index 0000000000..3d3b1a3e4d --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Data; +using Avalonia.Threading; + +namespace Avalonia.PropertyStore +{ + internal class DirectUntypedBindingObserver : IObserver, + IDisposable + { + private readonly ValueStore _owner; + private IDisposable? _subscription; + + public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase property) + { + _owner = owner; + Property = property; + } + + public DirectPropertyBase Property { get;} + + public void Start(IObservable source) + { + _subscription = source.Subscribe(this); + } + + public void Dispose() + { + _subscription?.Dispose(); + _subscription = null; + _owner.OnLocalValueBindingCompleted(Property, this); + } + + public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); + public void OnError(Exception error) => OnCompleted(); + + public void OnNext(object? value) + { + var typed = BindingValue.FromUntyped(value); + + if (Dispatcher.UIThread.CheckAccess()) + { + _owner.Owner.SetDirectValueUnchecked(Property, typed); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = _owner.Owner; + var property = Property; + var newValue = value; + Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, typed)); + } + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs new file mode 100644 index 0000000000..04d3c805c2 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -0,0 +1,168 @@ +using System.Diagnostics; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// Represents the active value for a property in a . + /// + /// + /// This class is an abstract base for the generic . + /// + internal abstract class EffectiveValue + { + private IValueEntry? _valueEntry; + private IValueEntry? _baseValueEntry; + + /// + /// Gets the current effective value as a boxed value. + /// + public object? Value => GetBoxedValue(); + + /// + /// Gets the priority of the current effective value. + /// + public BindingPriority Priority { get; protected set; } + + /// + /// Gets the priority of the current base value. + /// + public BindingPriority BasePriority { get; protected set; } + + /// + /// Begins a reevaluation pass on the effective value. + /// + /// + /// Determines whether any current local value should be cleared. + /// + /// + /// This method resets the and properties + /// to Unset, pending reevaluation. + /// + public void BeginReevaluation(bool clearLocalValue = false) + { + if (clearLocalValue || Priority != BindingPriority.LocalValue) + Priority = BindingPriority.Unset; + if (clearLocalValue || BasePriority != BindingPriority.LocalValue) + BasePriority = BindingPriority.Unset; + } + + /// + /// Ends a reevaluation pass on the effective value. + /// + /// + /// This method unsubscribes from any unused value entries. + /// + public void EndReevaluation() + { + if (Priority == BindingPriority.Unset) + { + _valueEntry?.Unsubscribe(); + _valueEntry = null; + } + + if (BasePriority == BindingPriority.Unset) + { + _baseValueEntry?.Unsubscribe(); + _baseValueEntry = null; + } + } + + /// + /// Sets the value and base value for a non-LocalValue priority, raising + /// where necessary. + /// + /// The associated value store. + /// The new value of the property. + /// The priority of the new value. + public abstract void SetAndRaise( + ValueStore owner, + IValueEntry value, + BindingPriority priority); + + /// + /// Raises in response to an inherited value + /// change. + /// + /// The owner object. + /// The property being changed. + /// The old value of the property. + /// The new value of the property. + public abstract void RaiseInheritedValueChanged( + AvaloniaObject owner, + AvaloniaProperty property, + EffectiveValue? oldValue, + EffectiveValue? newValue); + + /// + /// Removes the current animation value and reverts to the base value, raising + /// where necessary. + /// + /// The associated value store. + /// The property being changed. + public abstract void RemoveAnimationAndRaise(ValueStore owner, AvaloniaProperty property); + + /// + /// Coerces the property value. + /// + /// The associated value store. + /// The property to coerce. + public abstract void CoerceValue(ValueStore owner, AvaloniaProperty property); + + /// + /// Disposes the effective value, raising + /// where necessary. + /// + /// The associated value store. + /// The property being cleared. + public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property); + + protected abstract object? GetBoxedValue(); + + protected void UpdateValueEntry(IValueEntry? entry, BindingPriority priority) + { + Debug.Assert(priority != BindingPriority.LocalValue); + + if (priority <= BindingPriority.Animation) + { + // If we've received an animation value and the current value is a non-animation + // value, then the current entry becomes our base entry. + if (Priority > BindingPriority.LocalValue && Priority < BindingPriority.Inherited) + { + Debug.Assert(_valueEntry is not null); + _baseValueEntry = _valueEntry; + _valueEntry = null; + } + + if (_valueEntry != entry) + { + _valueEntry?.Unsubscribe(); + _valueEntry = entry; + } + } + else if (Priority <= BindingPriority.Animation) + { + // We've received a non-animation value and have an active animation value, so the + // new entry becomes our base entry. + if (_baseValueEntry != entry) + { + _baseValueEntry?.Unsubscribe(); + _baseValueEntry = entry; + } + } + else if (_valueEntry != entry) + { + // Both the current value and the new value are non-animation values, so the new + // entry replaces the existing entry. + _valueEntry?.Unsubscribe(); + _valueEntry = entry; + } + } + + protected void UnsubscribeValueEntries() + { + _valueEntry?.Unsubscribe(); + _baseValueEntry?.Unsubscribe(); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs new file mode 100644 index 0000000000..1d8f6b8408 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// Represents the active value for a property in a . + /// + /// + /// Stores the active value in an 's + /// for a single property, when the value is not inherited or unset/default. + /// + internal sealed class EffectiveValue : EffectiveValue + { + private readonly StyledPropertyMetadata _metadata; + private T? _baseValue; + private UncommonFields? _uncommon; + + public EffectiveValue(AvaloniaObject owner, StyledPropertyBase property) + { + Priority = BindingPriority.Unset; + BasePriority = BindingPriority.Unset; + _metadata = property.GetMetadata(owner.GetType()); + + var value = _metadata.DefaultValue; + + if (property.HasCoercion && _metadata.CoerceValue is { } coerce) + { + _uncommon = new() + { + _coerce = coerce, + _uncoercedValue = value, + _uncoercedBaseValue = value, + }; + + Value = coerce(owner, value); + } + else + { + Value = value; + } + } + + /// + /// Gets the current effective value. + /// + public new T Value { get; private set; } + + public override void SetAndRaise( + ValueStore owner, + IValueEntry value, + BindingPriority priority) + { + Debug.Assert(priority != BindingPriority.LocalValue); + UpdateValueEntry(value, priority); + + SetAndRaiseCore(owner, (StyledPropertyBase)value.Property, GetValue(value), priority); + } + + public void SetLocalValueAndRaise( + ValueStore owner, + StyledPropertyBase property, + T value) + { + SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); + } + + public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) + { + value = _baseValue!; + return BasePriority != BindingPriority.Unset; + } + + public override void RaiseInheritedValueChanged( + AvaloniaObject owner, + AvaloniaProperty property, + EffectiveValue? oldValue, + EffectiveValue? newValue) + { + Debug.Assert(oldValue is not null || newValue is not null); + + var p = (StyledPropertyBase)property; + var o = oldValue is not null ? ((EffectiveValue)oldValue).Value : _metadata.DefaultValue; + var n = newValue is not null ? ((EffectiveValue)newValue).Value : _metadata.DefaultValue; + var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset; + + if (!EqualityComparer.Default.Equals(o, n)) + { + owner.RaisePropertyChanged(p, o, n, priority, true); + } + } + + public override void RemoveAnimationAndRaise(ValueStore owner, AvaloniaProperty property) + { + Debug.Assert(Priority != BindingPriority.Animation); + Debug.Assert(BasePriority != BindingPriority.Unset); + UpdateValueEntry(null, BindingPriority.Animation); + SetAndRaiseCore(owner, (StyledPropertyBase)property, _baseValue!, BasePriority); + } + + public override void CoerceValue(ValueStore owner, AvaloniaProperty property) + { + if (_uncommon is null) + return; + SetAndRaiseCore( + owner, + (StyledPropertyBase)property, + _uncommon._uncoercedValue!, + Priority, + _uncommon._uncoercedBaseValue!, + BasePriority); + } + + public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) + { + UnsubscribeValueEntries(); + DisposeAndRaiseUnset(owner, (StyledPropertyBase)property); + } + + public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase property) + { + BindingPriority priority; + T oldValue; + + if (property.Inherits && owner.TryGetInheritedValue(property, out var i)) + { + oldValue = ((EffectiveValue)i).Value; + priority = BindingPriority.Inherited; + } + else + { + oldValue = _metadata.DefaultValue; + priority = BindingPriority.Unset; + } + + if (!EqualityComparer.Default.Equals(oldValue, Value)) + { + owner.Owner.RaisePropertyChanged(property, Value, oldValue, priority, true); + if (property.Inherits) + owner.OnInheritedEffectiveValueDisposed(property, Value); + } + } + + protected override object? GetBoxedValue() => Value; + + private static T GetValue(IValueEntry entry) + { + if (entry is IValueEntry typed) + return typed.GetValue(); + else + return (T)entry.GetValue()!; + } + + private void SetAndRaiseCore( + ValueStore owner, + StyledPropertyBase property, + T value, + BindingPriority priority) + { + Debug.Assert(priority < BindingPriority.Inherited); + + var oldValue = Value; + var valueChanged = false; + var baseValueChanged = false; + var v = value; + + if (_uncommon?._coerce is { } coerce) + v = coerce(owner.Owner, value); + + if (priority <= Priority) + { + valueChanged = !EqualityComparer.Default.Equals(Value, v); + Value = v; + Priority = priority; + if (_uncommon is not null) + _uncommon._uncoercedValue = value; + } + + if (priority <= BasePriority && priority >= BindingPriority.LocalValue) + { + baseValueChanged = !EqualityComparer.Default.Equals(_baseValue, v); + _baseValue = v; + BasePriority = priority; + if (_uncommon is not null) + _uncommon._uncoercedBaseValue = value; + } + + if (valueChanged) + { + using var notifying = PropertyNotifying.Start(owner.Owner, property); + owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true); + if (property.Inherits) + owner.OnInheritedEffectiveValueChanged(property, oldValue, this); + } + else if (baseValueChanged) + { + owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false); + } + } + + private void SetAndRaiseCore( + ValueStore owner, + StyledPropertyBase property, + T value, + BindingPriority priority, + T baseValue, + BindingPriority basePriority) + { + Debug.Assert(priority < BindingPriority.Inherited); + Debug.Assert(basePriority > BindingPriority.Animation); + Debug.Assert(priority <= basePriority); + + var oldValue = Value; + var valueChanged = false; + var baseValueChanged = false; + var v = value; + var bv = baseValue; + + if (_uncommon?._coerce is { } coerce) + { + v = coerce(owner.Owner, value); + bv = coerce(owner.Owner, baseValue); + } + + if (priority != BindingPriority.Unset && !EqualityComparer.Default.Equals(Value, v)) + { + Value = v; + valueChanged = true; + if (_uncommon is not null) + _uncommon._uncoercedValue = value; + } + + if (priority != BindingPriority.Unset && + (BasePriority == BindingPriority.Unset || + !EqualityComparer.Default.Equals(_baseValue, bv))) + { + _baseValue = v; + baseValueChanged = true; + if (_uncommon is not null) + _uncommon._uncoercedValue = baseValue; + } + + Priority = priority; + BasePriority = basePriority; + + if (valueChanged) + { + using var notifying = PropertyNotifying.Start(owner.Owner, property); + owner.Owner.RaisePropertyChanged(property, oldValue, Value, Priority, true); + if (property.Inherits) + owner.OnInheritedEffectiveValueChanged(property, oldValue, this); + } + + if (baseValueChanged) + { + owner.Owner.RaisePropertyChanged(property, default, _baseValue!, BasePriority, false); + } + } + + private class UncommonFields + { + public Func? _coerce; + public T? _uncoercedValue; + public T? _uncoercedBaseValue; + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs b/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs deleted file mode 100644 index af4faf989c..0000000000 --- a/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Avalonia.PropertyStore -{ - internal interface IBatchUpdate - { - void BeginBatchUpdate(); - void EndBatchUpdate(); - } -} diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs deleted file mode 100644 index 45bbd0cda5..0000000000 --- a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Avalonia.PropertyStore -{ - /// - /// Represents an untyped interface to . - /// - internal interface IPriorityValueEntry : IValue - { - } - - /// - /// Represents an object that can act as an entry in a . - /// - /// The property type. - internal interface IPriorityValueEntry : IPriorityValueEntry, IValue - { - void Reparent(PriorityValue parent); - } -} diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs deleted file mode 100644 index b493df92e6..0000000000 --- a/src/Avalonia.Base/PropertyStore/IValue.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents an untyped interface to . - /// - internal interface IValue - { - BindingPriority Priority { get; } - Optional GetValue(); - void Start(); - void RaiseValueChanged( - AvaloniaObject owner, - AvaloniaProperty property, - Optional oldValue, - Optional newValue); - } - - /// - /// Represents an object that can act as an entry in a . - /// - /// The property type. - internal interface IValue : IValue - { - Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation); - } -} diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry.cs b/src/Avalonia.Base/PropertyStore/IValueEntry.cs new file mode 100644 index 0000000000..271d85f8bc --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValueEntry.cs @@ -0,0 +1,30 @@ +using System; + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an untyped value entry in a . + /// + internal interface IValueEntry + { + bool HasValue { get; } + + /// + /// Gets the property that this value applies to. + /// + AvaloniaProperty Property { get; } + + /// + /// Gets the value associated with the entry. + /// + /// + /// The entry has no value. + /// + object? GetValue(); + + /// + /// Called when the value entry is removed from the value store. + /// + void Unsubscribe(); + } +} diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry`1.cs b/src/Avalonia.Base/PropertyStore/IValueEntry`1.cs new file mode 100644 index 0000000000..5b69009e79 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValueEntry`1.cs @@ -0,0 +1,16 @@ +namespace Avalonia.PropertyStore +{ + /// + /// Represents a typed value entry in a . + /// + internal interface IValueEntry : IValueEntry + { + /// + /// Gets the value associated with the entry. + /// + /// + /// The entry has no value. + /// + new T GetValue(); + } +} diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs new file mode 100644 index 0000000000..364b4e1225 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs @@ -0,0 +1,31 @@ +using System; + +namespace Avalonia.PropertyStore +{ + internal class ImmediateValueEntry : IValueEntry, IDisposable + { + private readonly ImmediateValueFrame _owner; + private readonly T _value; + + public ImmediateValueEntry( + ImmediateValueFrame owner, + StyledPropertyBase property, + T value) + { + _owner = owner; + _value = value; + Property = property; + } + + public StyledPropertyBase Property { get; } + public bool HasValue => true; + AvaloniaProperty IValueEntry.Property => Property; + + public void Unsubscribe() { } + + public void Dispose() => _owner.OnEntryDisposed(this); + + object? IValueEntry.GetValue() => _value; + T IValueEntry.GetValue() => _value; + } +} diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs new file mode 100644 index 0000000000..1d886e7501 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// Holds values in a set by one of the SetValue or AddBinding + /// overloads with non-LocalValue priority. + /// + internal class ImmediateValueFrame : ValueFrame + { + public ImmediateValueFrame(BindingPriority priority) + { + Priority = priority; + } + + public TypedBindingEntry AddBinding( + StyledPropertyBase property, + IObservable> source) + { + var e = new TypedBindingEntry(this, property, source); + Add(e); + return e; + } + + public TypedBindingEntry AddBinding( + StyledPropertyBase property, + IObservable source) + { + var e = new TypedBindingEntry(this, property, source); + Add(e); + return e; + } + + public SourceUntypedBindingEntry AddBinding( + StyledPropertyBase property, + IObservable source) + { + var e = new SourceUntypedBindingEntry(this, property, source); + Add(e); + return e; + } + + public ImmediateValueEntry AddValue(StyledPropertyBase property, T value) + { + var e = new ImmediateValueEntry(this, property, value); + Add(e); + return e; + } + + public void OnEntryDisposed(IValueEntry value) + { + Remove(value.Property); + Owner?.OnValueEntryRemoved(this, value.Property); + } + + protected override bool GetIsActive(out bool hasChanged) + { + hasChanged = false; + return true; + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs new file mode 100644 index 0000000000..4dca6c0100 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -0,0 +1,59 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + internal class LocalValueBindingObserver : IObserver, + IObserver>, + IDisposable + { + private readonly ValueStore _owner; + private IDisposable? _subscription; + + public LocalValueBindingObserver(ValueStore owner, StyledPropertyBase property) + { + _owner = owner; + Property = property; + } + + public StyledPropertyBase Property { get;} + + public void Start(IObservable source) + { + _subscription = source.Subscribe(this); + } + + public void Start(IObservable> 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 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); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs deleted file mode 100644 index 13ca69681f..0000000000 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Stores a value with local value priority in a or - /// . - /// - /// The property type. - internal class LocalValueEntry : IValue - { - private T _value; - - public LocalValueEntry(T value) => _value = value; - public BindingPriority Priority => BindingPriority.LocalValue; - Optional IValue.GetValue() => new Optional(_value); - - public Optional GetValue(BindingPriority maxPriority) - { - return BindingPriority.LocalValue >= maxPriority ? _value : Optional.Empty; - } - - public void SetValue(T value) => _value = value; - public void Start() { } - - public void RaiseValueChanged( - AvaloniaObject owner, - AvaloniaProperty property, - Optional oldValue, - Optional newValue) - { - owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - owner, - (AvaloniaProperty)property, - oldValue.Cast(), - newValue.Cast(), - BindingPriority.LocalValue)); - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs new file mode 100644 index 0000000000..099e997d38 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -0,0 +1,62 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + internal class LocalValueUntypedBindingObserver : IObserver, + IDisposable + { + private readonly ValueStore _owner; + private IDisposable? _subscription; + + public LocalValueUntypedBindingObserver(ValueStore owner, StyledPropertyBase property) + { + _owner = owner; + Property = property; + } + + public StyledPropertyBase Property { get; } + + public void Start(IObservable source) + { + _subscription = source.Subscribe(this); + } + + public void Dispose() + { + _subscription?.Dispose(); + _subscription = null; + _owner.OnLocalValueBindingCompleted(Property, this); + } + + public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); + public void OnError(Exception error) => OnCompleted(); + + public void OnNext(object? value) + { + if (value is BindingNotification n) + { + value = n.Value; + LoggingUtils.LogIfNecessary(_owner.Owner, Property, n); + } + + if (value == AvaloniaProperty.UnsetValue) + { + _owner.ClearLocalValue(Property); + } + else if (value == BindingOperations.DoNothing) + { + // Do nothing! + } + else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) + { + _owner.SetValue(Property, typedValue, BindingPriority.LocalValue); + } + else + { + _owner.ClearLocalValue(Property); + LoggingUtils.LogInvalidValue(_owner.Owner, Property, typeof(T), value); + } + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/LoggingUtils.cs b/src/Avalonia.Base/PropertyStore/LoggingUtils.cs new file mode 100644 index 0000000000..8b3015eca9 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LoggingUtils.cs @@ -0,0 +1,82 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + internal static class LoggingUtils + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogIfNecessary( + AvaloniaObject owner, + AvaloniaProperty property, + BindingNotification value) + { + if (value.ErrorType != BindingErrorType.None) + Log(owner, property, value.Error!); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogIfNecessary( + AvaloniaObject owner, + AvaloniaProperty property, + BindingValue value) + { + if (value.HasError) + Log(owner, property, value.Error!); + } + + public static void LogInvalidValue( + AvaloniaObject owner, + AvaloniaProperty property, + Type expectedType, + object? value) + { + if (value is not null) + { + owner.GetBindingWarningLogger(property, null)?.Log( + owner, + "Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})", + owner, + property, + expectedType, + value, + value.GetType()); + } + else + { + owner.GetBindingWarningLogger(property, null)?.Log( + owner, + "Error in binding to {Target}.{Property}: expected {ExpectedType}, got null", + owner, + property, + expectedType); + } + } + + private static void Log( + AvaloniaObject owner, + AvaloniaProperty property, + Exception e) + { + if (e is TargetInvocationException t) + e = t.InnerException!; + + if (e is AggregateException a) + { + foreach (var i in a.InnerExceptions) + Log(owner, property, i); + } + else + { + owner.GetBindingWarningLogger(property, e)?.Log( + owner, + "Error in binding to {Target}.{Property}: {Message}", + owner, + property, + e.Message); + } + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs deleted file mode 100644 index 182b2638c4..0000000000 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents an untyped interface to . - /// - interface IPriorityValue : IValue - { - void UpdateEffectiveValue(); - } - - /// - /// Stores a set of prioritized values and bindings in a . - /// - /// The property type. - /// - /// When more than a single value or binding is applied to a property in an - /// , the entry in the is converted into - /// a . This class holds any number of - /// entries (sorted first by priority and then in the order - /// they were added) plus a local value. - /// - internal class PriorityValue : IPriorityValue, IValue, IBatchUpdate - { - private readonly AvaloniaObject _owner; - private readonly ValueStore _store; - private readonly List> _entries = new List>(); - private readonly Func? _coerceValue; - private Optional _localValue; - private Optional _value; - private bool _isCalculatingValue; - private bool _batchUpdate; - - public PriorityValue( - AvaloniaObject owner, - StyledPropertyBase 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 property, - ValueStore store, - IPriorityValueEntry 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 property, - ValueStore sink, - LocalValueEntry existing) - : this(owner, property, sink) - { - _value = _localValue = existing.GetValue(BindingPriority.LocalValue); - Priority = BindingPriority.LocalValue; - } - - public StyledPropertyBase Property { get; } - public BindingPriority Priority { get; private set; } = BindingPriority.Unset; - public IReadOnlyList> Entries => _entries; - Optional 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( - _owner, - Property, - default, - default, - BindingPriority.LocalValue)); - } - - public Optional 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(Property, value, priority, new ValueOwner(this)); - _entries.Insert(insert, entry); - result = entry; - } - - UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( - _owner, - Property, - default, - value, - priority)); - - return result; - } - - public BindingEntry AddBinding(IObservable> source, BindingPriority priority) - { - var binding = new BindingEntry(_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 oldValue, - Optional newValue) - { - owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - owner, - (AvaloniaProperty)property, - oldValue.Cast(), - newValue.Cast(), - Priority)); - } - - public void ValueChanged(AvaloniaPropertyChangedEventArgs change) - { - if (change.Priority == BindingPriority.LocalValue) - { - _localValue = default; - } - - if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs c) - { - UpdateEffectiveValue(c); - } - } - - public void Completed(IPriorityValueEntry entry, Optional oldValue) - { - _entries.Remove((IPriorityValueEntry)entry); - UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( - _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, 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? 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( - _owner, - Property, - old, - value, - Priority)); - } - else if (change is object) - { - change.MarkNonEffectiveValue(); - change.SetOldValue(default); - _store.ValueChanged(change); - } - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/PropertyNotifying.cs b/src/Avalonia.Base/PropertyStore/PropertyNotifying.cs new file mode 100644 index 0000000000..f508059a74 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/PropertyNotifying.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics; + +namespace Avalonia.PropertyStore +{ + /// + /// Raises where necessary. + /// + /// + /// Uses the disposable pattern to ensure that the closing Notifying call is made even in the + /// presence of exceptions. + /// + internal readonly struct PropertyNotifying : IDisposable + { + private readonly AvaloniaObject _owner; + private readonly AvaloniaProperty _property; + + private PropertyNotifying(AvaloniaObject owner, AvaloniaProperty property) + { + Debug.Assert(property.Notifying is not null); + _owner = owner; + _property = property; + _property.Notifying!(owner, true); + } + + public void Dispose() => _property.Notifying!(_owner, false); + + public static PropertyNotifying? Start(AvaloniaObject owner, AvaloniaProperty property) + { + if (property.Notifying is null) + return null; + return new PropertyNotifying(owner, property); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs new file mode 100644 index 0000000000..b4ac06d2bf --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs @@ -0,0 +1,35 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// An that holds a binding whose source observable is untyped and + /// target property is typed. + /// + internal sealed class SourceUntypedBindingEntry : BindingEntryBase + { + private readonly Func? _validate; + + public SourceUntypedBindingEntry( + ValueFrame frame, + StyledPropertyBase property, + IObservable source) + : base(frame, property, source) + { + _validate = property.ValidateValue; + } + + public new StyledPropertyBase Property => (StyledPropertyBase)base.Property; + + protected override BindingValue ConvertAndValidate(object? value) + { + return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate); + } + + protected override BindingValue ConvertAndValidate(BindingValue value) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs new file mode 100644 index 0000000000..2276991a18 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs @@ -0,0 +1,52 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// An that holds a binding whose source observable and target + /// property are both typed. + /// + internal sealed class TypedBindingEntry : BindingEntryBase + { + public TypedBindingEntry( + ValueFrame frame, + StyledPropertyBase property, + IObservable source) + : base(frame, property, source) + { + } + + public TypedBindingEntry( + ValueFrame frame, + StyledPropertyBase property, + IObservable> source) + : base(frame, property, source) + { + } + + public new StyledPropertyBase Property => (StyledPropertyBase)base.Property; + + protected override BindingValue ConvertAndValidate(T value) + { + if (Property.ValidateValue?.Invoke(value) == false) + { + return BindingValue.BindingError( + new InvalidCastException($"'{value}' is not a valid value.")); + } + + return value; + } + + protected override BindingValue ConvertAndValidate(BindingValue value) + { + if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) + { + return BindingValue.BindingError( + new InvalidCastException($"'{value.Value}' is not a valid value.")); + } + + return value; + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs new file mode 100644 index 0000000000..f8becb2e06 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// An that holds a binding whose source observable and target + /// property are both untyped. + /// + internal class UntypedBindingEntry : BindingEntryBase + { + private readonly Func? _validate; + + public UntypedBindingEntry( + ValueFrame frame, + AvaloniaProperty property, + IObservable source) + : base(frame, property, source) + { + _validate = ((IStyledPropertyAccessor)property).ValidateValue; + } + + protected override BindingValue ConvertAndValidate(object? value) + { + return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate); + } + + protected override BindingValue ConvertAndValidate(BindingValue value) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs b/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs new file mode 100644 index 0000000000..acaecc0d52 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; +using Avalonia.Utilities; + +namespace Avalonia.PropertyStore +{ + internal static class UntypedValueUtils + { + public static BindingValue ConvertAndValidate( + object? value, + Type targetType, + Func? validate) + { + var v = BindingValue.FromUntyped(value, targetType); + + if (v.HasValue && validate?.Invoke(v.Value) == false) + { + return BindingValue.BindingError( + new InvalidCastException($"'{v.Value}' is not a valid value.")); + } + + return v; + } + + public static bool TryConvertAndValidate( + AvaloniaProperty property, + object? value, + out object? result) + { + if (TypeUtilities.TryConvertImplicit(property.PropertyType, value, out result)) + return ((IStyledPropertyAccessor)property).ValidateValue(result); + + result = default; + return false; + } + + public static bool TryConvertAndValidate( + StyledPropertyBase 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; + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/ValueFrame.cs b/src/Avalonia.Base/PropertyStore/ValueFrame.cs new file mode 100644 index 0000000000..5ada4b3c84 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ValueFrame.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; +using Avalonia.Utilities; +using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; + +namespace Avalonia.PropertyStore +{ + internal abstract class ValueFrame + { + private List? _entries; + private AvaloniaPropertyDictionary _index; + private ValueStore? _owner; + private bool _isShared; + + public int EntryCount => _index.Count; + public bool IsActive => GetIsActive(out _); + public ValueStore? Owner => !_isShared ? _owner : + throw new AvaloniaInternalException("Cannot get owner for shared ValueFrame"); + public BindingPriority Priority { get; protected set; } + + public bool Contains(AvaloniaProperty property) => _index.ContainsKey(property); + + public IValueEntry GetEntry(int index) => _entries?[index] ?? _index[0]; + + public void SetOwner(ValueStore? owner) + { + if (_owner is not null && owner is not null) + throw new AvaloniaInternalException("ValueFrame already has an owner."); + if (!_isShared) + _owner = owner; + } + + public bool TryGetEntryIfActive( + AvaloniaProperty property, + [NotNullWhen(true)] out IValueEntry? entry, + out bool activeChanged) + { + if (_index.TryGetValue(property, out entry)) + return GetIsActive(out activeChanged); + activeChanged = false; + return false; + } + + public void OnBindingCompleted(IValueEntry binding) + { + var property = binding.Property; + Remove(property); + Owner?.OnValueEntryRemoved(this, property); + } + + public virtual void Dispose() + { + for (var i = 0; i < _index.Count; ++i) + _index[i].Unsubscribe(); + } + + protected abstract bool GetIsActive(out bool hasChanged); + + protected void MakeShared() + { + _isShared = true; + _owner = null; + } + + protected void Add(IValueEntry value) + { + Debug.Assert(!value.Property.IsDirect); + + if (_entries is null && _index.Count == 1) + { + _entries = new(); + _entries.Add(_index[0]); + } + + _index.Add(value.Property, value); + _entries?.Add(value); + } + + protected void Remove(AvaloniaProperty property) + { + Debug.Assert(!property.IsDirect); + + if (_entries is not null) + { + var count = _entries.Count; + + for (var i = 0; i < count; ++i) + { + if (_entries[i].Property == property) + { + _entries.RemoveAt(i); + break; + } + } + } + + _index.Remove(property); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/ValueOwner.cs b/src/Avalonia.Base/PropertyStore/ValueOwner.cs deleted file mode 100644 index c68435f7a5..0000000000 --- a/src/Avalonia.Base/PropertyStore/ValueOwner.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents a union type of and , - /// which are the valid owners of a value store . - /// - /// The value type. - internal readonly struct ValueOwner - { - private readonly ValueStore? _store; - private readonly PriorityValue? _priorityValue; - - public ValueOwner(ValueStore o) - { - _store = o; - _priorityValue = null; - } - - public ValueOwner(PriorityValue v) - { - _store = null; - _priorityValue = v; - } - - public bool IsValueStore => _store is not null; - - public void Completed(StyledPropertyBase property, IPriorityValueEntry entry, Optional oldValue) - { - if (_store is not null) - _store?.Completed(property, entry, oldValue); - else - _priorityValue!.Completed(entry, oldValue); - } - - public void ValueChanged(AvaloniaPropertyChangedEventArgs e) - { - if (_store is not null) - _store?.ValueChanged(e); - else - _priorityValue!.ValueChanged(e); - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs new file mode 100644 index 0000000000..8790991182 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -0,0 +1,962 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; +using Avalonia.Diagnostics; +using Avalonia.Logging; +using Avalonia.Utilities; + +namespace Avalonia.PropertyStore +{ + internal class ValueStore + { + private readonly List _frames = new(); + private Dictionary? _localValueBindings; + private AvaloniaPropertyDictionary _effectiveValues; + private int _inheritedValueCount; + private int _isEvaluating; + private int _frameGeneration; + private int _styling; + + public ValueStore(AvaloniaObject owner) => Owner = owner; + + public AvaloniaObject Owner { get; } + public ValueStore? InheritanceAncestor { get; private set; } + public bool IsEvaluating => _isEvaluating > 0; + public IReadOnlyList Frames => _frames; + + public void BeginStyling() => ++_styling; + + public void EndStyling() + { + if (--_styling == 0) + ReevaluateEffectiveValues(); + } + + public void AddFrame(ValueFrame style) + { + InsertFrame(style); + ReevaluateEffectiveValues(); + } + + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable> source, + BindingPriority priority) + { + if (priority == BindingPriority.LocalValue) + { + var observer = new LocalValueBindingObserver(this, property); + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = observer; + observer.Start(source); + return observer; + } + else + { + var effective = GetEffectiveValue(property); + + var frame = GetOrCreateImmediateValueFrame(property, priority, out _); + var result = frame.AddBinding(property, source); + + if (effective is null || priority <= effective.Priority) + result.Start(); + + return result; + } + } + + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable source, + BindingPriority priority) + { + if (priority == BindingPriority.LocalValue) + { + var observer = new LocalValueBindingObserver(this, property); + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = observer; + observer.Start(source); + return observer; + } + else + { + var effective = GetEffectiveValue(property); + + var frame = GetOrCreateImmediateValueFrame(property, priority, out _); + var result = frame.AddBinding(property, source); + + if (effective is null || priority <= effective.Priority) + result.Start(); + + return result; + } + } + + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable source, + BindingPriority priority) + { + if (priority == BindingPriority.LocalValue) + { + var observer = new LocalValueUntypedBindingObserver(this, property); + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = observer; + observer.Start(source); + return observer; + } + else + { + var effective = GetEffectiveValue(property); + + var frame = GetOrCreateImmediateValueFrame(property, priority, out _); + var result = frame.AddBinding(property, source); + + if (effective is null || priority <= effective.Priority) + result.Start(); + + return result; + } + } + + public IDisposable AddBinding(DirectPropertyBase property, IObservable> source) + { + var observer = new DirectBindingObserver(this, property); + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = observer; + observer.Start(source); + return observer; + } + + public IDisposable AddBinding(DirectPropertyBase property, IObservable source) + { + var observer = new DirectBindingObserver(this, property); + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = observer; + observer.Start(source); + return observer; + } + + public IDisposable AddBinding(DirectPropertyBase property, IObservable source) + { + var observer = new DirectUntypedBindingObserver(this, property); + DisposeExistingLocalValueBinding(property); + _localValueBindings ??= new(); + _localValueBindings[property.Id] = observer; + observer.Start(source); + return observer; + } + + public void ClearLocalValue(AvaloniaProperty property) + { + if (TryGetEffectiveValue(property, out var effective) && + effective.Priority == BindingPriority.LocalValue) + { + ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true); + } + } + + public IDisposable? SetValue(StyledPropertyBase property, T value, BindingPriority priority) + { + if (property.ValidateValue?.Invoke(value) == false) + { + throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); + } + + if (priority != BindingPriority.LocalValue) + { + var frame = GetOrCreateImmediateValueFrame(property, priority, out _); + var result = frame.AddValue(property, value); + + if (TryGetEffectiveValue(property, out var existing)) + { + var effective = (EffectiveValue)existing; + effective.SetAndRaise(this, result, priority); + } + else + { + var effectiveValue = new EffectiveValue(Owner, property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetAndRaise(this, result, priority); + } + + return result; + } + else + { + if (TryGetEffectiveValue(property, out var existing)) + { + var effective = (EffectiveValue)existing; + effective.SetLocalValueAndRaise(this, property, value); + } + else + { + var effectiveValue = new EffectiveValue(Owner, property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetLocalValueAndRaise(this, property, value); + } + + return null; + } + } + + public object? GetValue(AvaloniaProperty property) + { + if (_effectiveValues.TryGetValue(property, out var v)) + return v.Value; + if (property.Inherits && TryGetInheritedValue(property, out v)) + return v.Value; + + return GetDefaultValue(property); + } + + public T GetValue(StyledPropertyBase property) + { + if (_effectiveValues.TryGetValue(property, out var v)) + return ((EffectiveValue)v).Value; + if (property.Inherits && TryGetInheritedValue(property, out v)) + return ((EffectiveValue)v).Value; + return property.GetDefaultValue(Owner.GetType()); + } + + public bool IsAnimating(AvaloniaProperty property) + { + if (_effectiveValues.TryGetValue(property, out var v)) + return v.Priority <= BindingPriority.Animation; + return false; + } + + public bool IsSet(AvaloniaProperty property) + { + if (_effectiveValues.TryGetValue(property, out var v)) + return v.Priority < BindingPriority.Inherited; + return false; + } + + public void CoerceValue(AvaloniaProperty property) + { + if (_effectiveValues.TryGetValue(property, out var v)) + v.CoerceValue(this, property); + } + + public Optional GetBaseValue(StyledPropertyBase property) + { + if (TryGetEffectiveValue(property, out var v) && + ((EffectiveValue)v).TryGetBaseValue(out var baseValue)) + { + return baseValue; + } + + return default; + } + + public bool TryGetInheritedValue( + AvaloniaProperty property, + [NotNullWhen(true)] out EffectiveValue? result) + { + Debug.Assert(property.Inherits); + + var i = InheritanceAncestor; + + while (i is not null) + { + if (i.TryGetEffectiveValue(property, out result)) + return true; + i = i.InheritanceAncestor; + } + + result = null; + return false; + } + + public void SetInheritanceParent(AvaloniaObject? newParent) + { + var values = AvaloniaPropertyDictionaryPool.Get(); + var oldAncestor = InheritanceAncestor; + var newAncestor = newParent?.GetValueStore(); + + if (newAncestor?._inheritedValueCount == 0) + newAncestor = newAncestor.InheritanceAncestor; + + // The old and new inheritance ancestors are the same, nothing to do here. + if (oldAncestor == newAncestor) + return; + + // First get the old values from the old inheritance ancestor. + var f = oldAncestor; + + while (f is not null) + { + var count = f._effectiveValues.Count; + + for (var i = 0; i < count; ++i) + { + f._effectiveValues.GetKeyValue(i, out var key, out var value); + if (key.Inherits) + values.TryAdd(key, new(value)); + } + + f = f.InheritanceAncestor; + } + + f = newAncestor; + + // Get the new values from the new inheritance ancestor. + while (f is not null) + { + var count = f._effectiveValues.Count; + + for (var i = 0; i < count; ++i) + { + f._effectiveValues.GetKeyValue(i, out var key, out var value); + + if (!key.Inherits) + continue; + + if (values.TryGetValue(key, out var existing)) + { + if (existing.NewValue is null) + values[key] = existing.WithNewValue(value); + } + else + { + values.Add(key, new(null, value)); + } + } + + f = f.InheritanceAncestor; + } + + OnInheritanceAncestorChanged(newAncestor); + + // Raise PropertyChanged events where necessary on this object and inheritance children. + { + var count = values.Count; + for (var i = 0; i < count; ++i) + { + values.GetKeyValue(i, out var key, out var v); + var oldValue = v.OldValue; + var newValue = v.NewValue; + + if (oldValue != newValue) + InheritedValueChanged(key, oldValue, newValue); + } + } + + AvaloniaPropertyDictionaryPool.Release(values); + } + + /// + /// Called by non-LocalValue binding entries to re-evaluate the effective value when the + /// binding produces a new value. + /// + /// The binding entry. + /// The priority of binding which produced a new value. + public void OnBindingValueChanged( + IValueEntry entry, + BindingPriority priority) + { + Debug.Assert(priority != BindingPriority.LocalValue); + + var property = entry.Property; + + if (TryGetEffectiveValue(property, out var existing)) + { + if (priority <= existing.BasePriority) + ReevaluateEffectiveValue(property, existing); + } + else + { + AddEffectiveValueAndRaise(property, entry, priority); + } + } + + /// + /// Called by non-LocalValue binding entries to re-evaluate the effective value when the + /// binding produces an unset value. + /// + /// The bound property. + /// The priority of binding which produced a new value. + 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); + } + } + + /// + /// Called by a when its + /// state changes. + /// + /// The frame which produced the change. + public void OnFrameActivationChanged(ValueFrame frame) + { + if (frame.EntryCount == 0) + return; + else if (frame.EntryCount == 1) + { + var property = frame.GetEntry(0).Property; + _effectiveValues.TryGetValue(property, out var current); + ReevaluateEffectiveValue(property, current); + } + else + ReevaluateEffectiveValues(); + } + + /// + /// Called by the parent value store when its inheritance ancestor changes. + /// + /// The new inheritance ancestor. + public void OnInheritanceAncestorChanged(ValueStore? ancestor) + { + if (ancestor != this) + { + InheritanceAncestor = ancestor; + if (_inheritedValueCount > 0) + return; + } + + var children = Owner.GetInheritanceChildren(); + + if (children is null) + return; + + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().OnInheritanceAncestorChanged(ancestor); + } + } + + /// + /// Called by when an property with inheritance enabled + /// changes its value on this value store. + /// + /// The property whose value changed. + /// The old value of the property. + /// The effective value instance. + public void OnInheritedEffectiveValueChanged( + StyledPropertyBase property, + T oldValue, + EffectiveValue value) + { + Debug.Assert(property.Inherits); + + var children = Owner.GetInheritanceChildren(); + + if (children is null) + return; + + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, value.Value); + } + } + + /// + /// Called by when an property with inheritance enabled + /// is removed from the effective values. + /// + /// The property whose value changed. + /// The old value of the property. + public void OnInheritedEffectiveValueDisposed(StyledPropertyBase property, T oldValue) + { + Debug.Assert(property.Inherits); + + var children = Owner.GetInheritanceChildren(); + + if (children is not null) + { + var defaultValue = property.GetDefaultValue(Owner.GetType()); + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, defaultValue); + } + } + } + + /// + /// Called when a or + /// completes. + /// + /// The previously bound property. + /// The observer. + 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); + } + } + } + + /// + /// Called when an inherited property changes on the value store of the inheritance ancestor. + /// + /// The property type. + /// The property. + /// The old value of the property. + /// The new value of the property. + public void OnAncestorInheritedValueChanged( + StyledPropertyBase property, + T oldValue, + T newValue) + { + Debug.Assert(property.Inherits); + + // If the inherited value is set locally, propagation stops here. + if (_effectiveValues.ContainsKey(property)) + return; + + using var notifying = PropertyNotifying.Start(Owner, property); + + Owner.RaisePropertyChanged( + property, + oldValue, + newValue, + BindingPriority.Inherited, + true); + + var children = Owner.GetInheritanceChildren(); + + if (children is null) + return; + + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue); + } + } + + /// + /// Called by a to re-evaluate the effective value when a value + /// is removed. + /// + /// The frame on which the change occurred. + /// The property whose value was removed. + public void OnValueEntryRemoved(ValueFrame frame, AvaloniaProperty property) + { + if (frame.EntryCount == 0) + _frames.Remove(frame); + + if (TryGetEffectiveValue(property, out var existing)) + { + if (frame.Priority <= existing.Priority) + ReevaluateEffectiveValue(property, existing); + } + } + + public bool RemoveFrame(ValueFrame frame) + { + if (_frames.Remove(frame)) + { + frame.Dispose(); + ++_frameGeneration; + ReevaluateEffectiveValues(); + } + + return false; + } + + public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property) + { + object? value; + BindingPriority priority; + + if (_effectiveValues.TryGetValue(property, out var v)) + { + value = v.Value; + priority = v.Priority; + } + else if (property.Inherits && TryGetInheritedValue(property, out v)) + { + value = v.Value; + priority = BindingPriority.Inherited; + } + else + { + value = GetDefaultValue(property); + priority = BindingPriority.Unset; + } + + return new AvaloniaPropertyValue( + property, + value, + priority, + null); + } + + private int InsertFrame(ValueFrame frame) + { + // Uncomment this line when #8549 is fixed. + //Debug.Assert(!_frames.Contains(frame)); + + var index = BinarySearchFrame(frame.Priority); + _frames.Insert(index, frame); + ++_frameGeneration; + frame.SetOwner(this); + return index; + } + + private ImmediateValueFrame GetOrCreateImmediateValueFrame( + AvaloniaProperty property, + BindingPriority priority, + out int frameIndex) + { + Debug.Assert(priority != BindingPriority.LocalValue); + + var index = BinarySearchFrame(priority); + + if (index > 0 && _frames[index - 1] is ImmediateValueFrame f && + f.Priority == priority && + !f.Contains(property)) + { + frameIndex = index - 1; + return f; + } + + var result = new ImmediateValueFrame(priority); + frameIndex = InsertFrame(result); + return result; + } + + private void AddEffectiveValue(AvaloniaProperty property, EffectiveValue effectiveValue) + { + _effectiveValues.Add(property, effectiveValue); + + if (property.Inherits && _inheritedValueCount++ == 0) + OnInheritanceAncestorChanged(this); + } + + /// + /// Adds a new effective value, raises the initial + /// event and notifies inheritance children if necessary . + /// + /// The property. + /// The value entry. + /// The value priority. + private void AddEffectiveValueAndRaise(AvaloniaProperty property, IValueEntry entry, BindingPriority priority) + { + Debug.Assert(priority < BindingPriority.Inherited); + var effectiveValue = property.CreateEffectiveValue(Owner); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetAndRaise(this, entry, priority); + } + + private void RemoveEffectiveValue(AvaloniaProperty property, int index) + { + _effectiveValues.RemoveAt(index); + if (property.Inherits && --_inheritedValueCount == 0) + OnInheritanceAncestorChanged(InheritanceAncestor); + } + + private bool RemoveEffectiveValue(AvaloniaProperty property) + { + if (_effectiveValues.Remove(property)) + { + if (property.Inherits && --_inheritedValueCount == 0) + OnInheritanceAncestorChanged(InheritanceAncestor); + return true; + } + + return false; + } + + private void InheritedValueChanged( + AvaloniaProperty property, + EffectiveValue? oldValue, + EffectiveValue? newValue) + { + Debug.Assert(oldValue != newValue); + Debug.Assert(oldValue is not null || newValue is not null); + + // If the value is set locally, propagaton ends here. + if (_effectiveValues.ContainsKey(property) == true) + return; + + using var notifying = PropertyNotifying.Start(Owner, property); + + // Raise PropertyChanged on this object if necessary. + (oldValue ?? newValue!).RaiseInheritedValueChanged(Owner, property, oldValue, newValue); + + var children = Owner.GetInheritanceChildren(); + + if (children is null) + return; + + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + children[i].GetValueStore().InheritedValueChanged(property, oldValue, newValue); + } + } + + private void ReevaluateEffectiveValue( + AvaloniaProperty property, + EffectiveValue? current, + bool ignoreLocalValue = false) + { + ++_isEvaluating; + + try + { + restart: + // Don't reevaluate if a styling pass is in effect, reevaluation will be done when + // it has finished. + if (_styling > 0) + return; + + var generation = _frameGeneration; + + // Notify the existing effective value that reevaluation is starting. + current?.BeginReevaluation(ignoreLocalValue); + + // Iterate the frames to get the effective value. + for (var i = _frames.Count - 1; i >= 0; --i) + { + var frame = _frames[i]; + var priority = frame.Priority; + var foundEntry = frame.TryGetEntryIfActive(property, out var entry, out var activeChanged); + + // If the active state of the frame has changed since the last read, and + // the frame holds multiple values then we need to re-evaluate the + // effective values of all properties. + if (activeChanged && frame.EntryCount > 1) + { + ReevaluateEffectiveValues(); + return; + } + + // We're interested in the value if: + // - There is no current effective value, or + // - The value's priority is higher than the current effective value's priority, or + // - The value is a non-animation value and its priority is higher than the current + // effective value's base priority + var isRelevantPriority = current is null || + priority < current.Priority || + (priority > BindingPriority.Animation && priority < current.BasePriority); + + if (foundEntry && isRelevantPriority && entry!.HasValue) + { + if (current is not null) + { + current.SetAndRaise(this, entry, priority); + } + else + { + current = property.CreateEffectiveValue(Owner); + AddEffectiveValue(property, current); + current.SetAndRaise(this, entry, priority); + } + } + + if (generation != _frameGeneration) + goto restart; + + if (current?.Priority < BindingPriority.Unset && + current?.BasePriority < BindingPriority.Unset) + break; + } + + current?.EndReevaluation(); + + if (current?.Priority == BindingPriority.Unset) + { + if (current.BasePriority == BindingPriority.Unset) + { + RemoveEffectiveValue(property); + current.DisposeAndRaiseUnset(this, property); + } + else + { + current.RemoveAnimationAndRaise(this, property); + } + } + } + finally + { + --_isEvaluating; + } + } + + private void ReevaluateEffectiveValues() + { + ++_isEvaluating; + + try + { + restart: + // Don't reevaluate if a styling pass is in effect, reevaluation will be done when + // it has finished. + if (_styling > 0) + return; + + var generation = _frameGeneration; + var count = _effectiveValues.Count; + + // Notify the existing effective values that reevaluation is starting. + for (var i = 0; i < count; ++i) + _effectiveValues[i].BeginReevaluation(); + + // Iterate the frames, setting and creating effective values. + for (var i = _frames.Count - 1; i >= 0; --i) + { + var frame = _frames[i]; + + if (!frame.IsActive) + continue; + + var priority = frame.Priority; + + count = frame.EntryCount; + + for (var j = 0; j < count; ++j) + { + var entry = frame.GetEntry(j); + var property = entry.Property; + + // Skip if we already have a value/base value for this property. + if (_effectiveValues.TryGetValue(property, out var effectiveValue) && + effectiveValue.BasePriority < BindingPriority.Unset) + continue; + + if (!entry.HasValue) + continue; + + if (effectiveValue is not null) + { + effectiveValue.SetAndRaise(this, entry, priority); + } + else + { + var v = property.CreateEffectiveValue(Owner); + AddEffectiveValue(property, v); + v.SetAndRaise(this, entry, priority); + } + + if (generation != _frameGeneration) + goto restart; + } + } + + // Remove all effective values that are still unset. + for (var i = _effectiveValues.Count - 1; i >= 0; --i) + { + _effectiveValues.GetKeyValue(i, out var key, out var e); + e.EndReevaluation(); + + if (e.Priority == BindingPriority.Unset) + { + RemoveEffectiveValue(key, i); + e.DisposeAndRaiseUnset(this, key); + + if (i > _effectiveValues.Count) + break; + } + } + } + finally + { + --_isEvaluating; + } + } + + private bool TryGetEffectiveValue( + AvaloniaProperty property, + [NotNullWhen(true)] out EffectiveValue? value) + { + if (_effectiveValues.TryGetValue(property, out value)) + return true; + value = null; + return false; + } + + private EffectiveValue? GetEffectiveValue(AvaloniaProperty property) + { + if (_effectiveValues.TryGetValue(property, out var value)) + return value; + return null; + } + + private object? GetDefaultValue(AvaloniaProperty property) + { + return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner.GetType()); + } + + private void DisposeExistingLocalValueBinding(AvaloniaProperty property) + { + if (_localValueBindings is not null && + _localValueBindings.TryGetValue(property.Id, out var existing)) + { + existing.Dispose(); + } + } + + private int BinarySearchFrame(BindingPriority priority) + { + var lo = 0; + var hi = _frames.Count - 1; + + // Binary search insertion point. + while (lo <= hi) + { + var i = lo + ((hi - lo) >> 1); + var order = priority - _frames[i].Priority; + + if (order <= 0) + { + lo = i + 1; + } + else + { + hi = i - 1; + } + } + + return lo; + } + + private readonly struct OldNewValue + { + public OldNewValue(EffectiveValue? oldValue) + { + OldValue = oldValue; + NewValue = null; + } + + public OldNewValue(EffectiveValue? oldValue, EffectiveValue? newValue) + { + OldValue = oldValue; + NewValue = newValue; + } + + public readonly EffectiveValue? OldValue; + public readonly EffectiveValue? NewValue; + + public OldNewValue WithNewValue(EffectiveValue newValue) => new(OldValue, newValue); + } + } +} diff --git a/src/Avalonia.Base/Reactive/BindingValueAdapter.cs b/src/Avalonia.Base/Reactive/BindingValueAdapter.cs deleted file mode 100644 index 8eaf21dcdc..0000000000 --- a/src/Avalonia.Base/Reactive/BindingValueAdapter.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Reactive.Subjects; -using Avalonia.Data; - -namespace Avalonia.Reactive -{ - internal class BindingValueAdapter : SingleSubscriberObservableBase>, - IObserver - { - private readonly IObservable _source; - private IDisposable? _subscription; - - public BindingValueAdapter(IObservable source) => _source = source; - public void OnCompleted() => PublishCompleted(); - public void OnError(Exception error) => PublishError(error); - public void OnNext(T value) => PublishNext(BindingValue.FromUntyped(value)); - protected override void Subscribed() => _subscription = _source.Subscribe(this); - protected override void Unsubscribed() => _subscription?.Dispose(); - } - - internal class BindingValueSubjectAdapter : SingleSubscriberObservableBase>, - ISubject> - { - private readonly ISubject _source; - private readonly Inner _inner; - private IDisposable? _subscription; - - public BindingValueSubjectAdapter(ISubject source) - { - _source = source; - _inner = new Inner(this); - } - - public void OnCompleted() => _source.OnCompleted(); - public void OnError(Exception error) => _source.OnError(error); - - public void OnNext(BindingValue value) - { - if (value.HasValue) - { - _source.OnNext(value.Value); - } - } - - protected override void Subscribed() => _subscription = _source.Subscribe(_inner); - protected override void Unsubscribed() => _subscription?.Dispose(); - - private class Inner : IObserver - { - private readonly BindingValueSubjectAdapter _owner; - - public Inner(BindingValueSubjectAdapter owner) => _owner = owner; - - public void OnCompleted() => _owner.PublishCompleted(); - public void OnError(Exception error) => _owner.PublishError(error); - public void OnNext(T value) => _owner.PublishNext(BindingValue.FromUntyped(value)); - } - } -} diff --git a/src/Avalonia.Base/Reactive/BindingValueExtensions.cs b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs deleted file mode 100644 index 770da2cab4..0000000000 --- a/src/Avalonia.Base/Reactive/BindingValueExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Reactive.Subjects; -using Avalonia.Data; - -namespace Avalonia.Reactive -{ - public static class BindingValueExtensions - { - public static IObservable> ToBindingValue(this IObservable source) - { - source = source ?? throw new ArgumentNullException(nameof(source)); - return new BindingValueAdapter(source); - } - - public static ISubject> ToBindingValue(this ISubject source) - { - source = source ?? throw new ArgumentNullException(nameof(source)); - return new BindingValueSubjectAdapter(source); - } - - public static IObservable ToUntyped(this IObservable> source) - { - source = source ?? throw new ArgumentNullException(nameof(source)); - return new UntypedBindingAdapter(source); - } - - public static ISubject ToUntyped(this ISubject> source) - { - source = source ?? throw new ArgumentNullException(nameof(source)); - return new UntypedBindingSubjectAdapter(source); - } - } -} diff --git a/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs index 5e3a6bf79a..53a0b43c63 100644 --- a/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs +++ b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs @@ -51,10 +51,11 @@ namespace Avalonia.Reactive protected void PublishCompleted() { + _completed = true; + if (_observer != null) { _observer.OnCompleted(); - _completed = true; Unsubscribed(); _observer = null; } @@ -62,10 +63,11 @@ namespace Avalonia.Reactive protected void PublishError(Exception error) { + _error = error; + if (_observer != null) { _observer.OnError(error); - _error = error; Unsubscribed(); _observer = null; } diff --git a/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs deleted file mode 100644 index f75917a00e..0000000000 --- a/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using Avalonia.Data; -using Avalonia.Logging; - -namespace Avalonia.Reactive -{ - internal class TypedBindingAdapter : SingleSubscriberObservableBase>, - IObserver> - { - private readonly IAvaloniaObject _target; - private readonly AvaloniaProperty _property; - private readonly IObservable> _source; - private IDisposable? _subscription; - - public TypedBindingAdapter( - IAvaloniaObject target, - AvaloniaProperty property, - IObservable> source) - { - _target = target; - _property = property; - _source = source; - } - - public void OnNext(BindingValue value) - { - try - { - PublishNext(value.Convert()); - } - catch (InvalidCastException e) - { - var unwrappedValue = value.HasValue ? value.Value : null; - - Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log( - _target, - "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", - _property.Name, - _property.PropertyType, - unwrappedValue, - unwrappedValue?.GetType()); - PublishNext(BindingValue.BindingError(e)); - } - } - - public void OnCompleted() => PublishCompleted(); - public void OnError(Exception error) => PublishError(error); - - public static IObservable> Create( - IAvaloniaObject target, - AvaloniaProperty property, - IObservable> source) - { - return source is IObservable> result ? - result : - new TypedBindingAdapter(target, property, source); - } - - protected override void Subscribed() => _subscription = _source.Subscribe(this); - protected override void Unsubscribed() => _subscription?.Dispose(); - } -} diff --git a/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs deleted file mode 100644 index 58fbee7b8f..0000000000 --- a/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Reactive.Subjects; -using Avalonia.Data; - -namespace Avalonia.Reactive -{ - internal class UntypedBindingAdapter : SingleSubscriberObservableBase, - IObserver> - { - private readonly IObservable> _source; - private IDisposable? _subscription; - - public UntypedBindingAdapter(IObservable> source) => _source = source; - public void OnCompleted() => PublishCompleted(); - public void OnError(Exception error) => PublishError(error); - public void OnNext(BindingValue value) => value.ToUntyped(); - protected override void Subscribed() => _subscription = _source.Subscribe(this); - protected override void Unsubscribed() => _subscription?.Dispose(); - } - - internal class UntypedBindingSubjectAdapter : SingleSubscriberObservableBase, - ISubject - { - private readonly ISubject> _source; - private readonly Inner _inner; - private IDisposable? _subscription; - - public UntypedBindingSubjectAdapter(ISubject> source) - { - _source = source; - _inner = new Inner(this); - } - - public void OnCompleted() => _source.OnCompleted(); - public void OnError(Exception error) => _source.OnError(error); - public void OnNext(object? value) - { - _source.OnNext(BindingValue.FromUntyped(value)); - } - - protected override void Subscribed() => _subscription = _source.Subscribe(_inner); - protected override void Unsubscribed() => _subscription?.Dispose(); - - private class Inner : IObserver> - { - private readonly UntypedBindingSubjectAdapter _owner; - - public Inner(UntypedBindingSubjectAdapter owner) => _owner = owner; - - public void OnCompleted() => _owner.PublishCompleted(); - public void OnError(Exception error) => _owner.PublishError(error); - public void OnNext(BindingValue value) => _owner.PublishNext(value.ToUntyped()); - } - } -} diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 3a42e9bdfe..bba9685ed8 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/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? _appliedStyles; private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; private bool _hasPromotedTheme; @@ -333,7 +333,7 @@ namespace Avalonia throw new InvalidOperationException("BeginInit was not called."); } - if (--_initCount == 0 && _logicalRoot != null) + if (--_initCount == 0 && _logicalRoot is not null) { ApplyStyling(); InitializeIfNeeded(); @@ -351,18 +351,25 @@ namespace Avalonia { if (_initCount == 0 && !_styled) { - try - { - BeginBatchUpdate(); - AvaloniaLocator.Current.GetService()?.ApplyStyles(this); - } - finally + var styler = AvaloniaLocator.Current.GetService(); + var hasPromotedTheme = _hasPromotedTheme; + + if (styler is object) { - _styled = true; - EndBatchUpdate(); + GetValueStore().BeginStyling(); + + try + { + styler.ApplyStyles(this); + } + finally + { + _styled = true; + GetValueStore().EndStyling(); + } } - if (_hasPromotedTheme) + if (hasPromotedTheme) { _hasPromotedTheme = false; ClearValue(ThemeProperty); @@ -389,14 +396,15 @@ namespace Avalonia internal StyleDiagnostics GetStyleDiagnosticsInternal() { - IReadOnlyList? appliedStyles = _appliedStyles; + var styles = new List(); - if (appliedStyles is null) + foreach (var frame in GetValueStore().Frames) { - appliedStyles = Array.Empty(); + if (frame is IStyleInstance style) + styles.Add(style); } - return new StyleDiagnostics(appliedStyles); + return new StyleDiagnostics(styles); } /// @@ -522,20 +530,8 @@ namespace Avalonia return null; } - void IStyleable.StyleApplied(IStyleInstance instance) - { - instance = instance ?? throw new ArgumentNullException(nameof(instance)); - - _appliedStyles ??= new List(); - _appliedStyles.Add(instance); - } - void IStyleable.DetachStyles() => DetachStyles(); - void IStyleable.DetachStyles(IReadOnlyList styles) => DetachStyles(styles); - - void IStyleable.InvalidateStyles() => InvalidateStyles(); - void IStyleHost.StylesAdded(IReadOnlyList styles) { InvalidateStylesOnThisAndDescendents(); @@ -643,13 +639,15 @@ namespace Avalonia if (change.Property == ThemeProperty) { + var (oldValue, newValue) = change.GetOldAndNewValue(); + // Changing the theme detaches all styles, meaning that if the theme property was // set via a style, it will get cleared! To work around this, if the value was // applied at less than local value priority then promote the value to local value // priority until styling is re-applied. if (change.Priority > BindingPriority.LocalValue) { - Theme = change.GetNewValue(); + Theme = newValue; _hasPromotedTheme = true; } else if (_hasPromotedTheme && change.Priority == BindingPriority.LocalValue) @@ -658,9 +656,16 @@ namespace Avalonia } InvalidateStyles(); + + if (oldValue is not null) + DetachControlThemeFromTemplateChildren(oldValue); } } + internal virtual void DetachControlThemeFromTemplateChildren(ControlTheme theme) + { + } + private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) { if (o is StyledElement element) @@ -830,59 +835,25 @@ namespace Avalonia } } - private void DetachStyles() + private void DetachStyles(IReadOnlyList? 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 styles) - { - styles = styles ?? throw new ArgumentNullException(nameof(styles)); - - if (_appliedStyles is null) - { - return; - } - - var count = styles.Count; - - for (var i = 0; i < count; ++i) - { - for (var j = _appliedStyles.Count - 1; j >= 0; --j) - { - var applied = _appliedStyles[j]; - - if (applied.Source == styles[i]) - { - _appliedStyles.RemoveAt(j); - applied.Dispose(); - } - - if (j > _appliedStyles.Count) - j = _appliedStyles.Count; - } - } - } - private void InvalidateStylesOnThisAndDescendents() { InvalidateStyles(); @@ -898,7 +869,7 @@ namespace Avalonia } } - private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) + private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) { DetachStyles(styles); @@ -930,38 +901,24 @@ namespace Avalonia } } - private static IReadOnlyList RecurseStyles(IReadOnlyList styles) + private static IReadOnlyList RecurseStyles(IReadOnlyList styles) { - var count = styles.Count; - List? 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(styles); - } - - RecurseStyles(style.Children, result); - } - } - - return result ?? styles; + var result = new List(); + RecurseStyles(styles, result); + return result; } - private static void RecurseStyles(IReadOnlyList styles, List result) + private static void RecurseStyles(IReadOnlyList styles, List 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 children) + RecurseStyles(children, result); } } } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index da607720ff..6fb2f79918 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/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 /// 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(o, this); + } + /// internal override void RouteClearValue(AvaloniaObject o) { @@ -182,77 +199,44 @@ namespace Avalonia } /// - internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(AvaloniaObject o) { - var value = o.GetBaseValue(this, maxPriority); + var value = o.GetBaseValue(this); return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; } /// 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(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(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 o, - IObservable> source, + AvaloniaObject target, + IObservable source, BindingPriority priority) { - var adapter = TypedBindingAdapter.Create(o, this, source); - return o.Bind(this, adapter, priority); - } - - /// - 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( - target, - this, - binding); - } - else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType)) - { - return new PropertySetterTemplateInstance( - target, - this, - template); - } - else - { - return new PropertySetterInstance( - target, - this, - (TValue)value!); - } + return target.Bind(this, source, priority); } private object? GetDefaultBoxedValue(Type type) diff --git a/src/Avalonia.Base/Styling/Activators/AndActivator.cs b/src/Avalonia.Base/Styling/Activators/AndActivator.cs index 0e1e3b565b..6ca9c10858 100644 --- a/src/Avalonia.Base/Styling/Activators/AndActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/AndActivator.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Collections.Generic; +using System.Collections.Generic; namespace Avalonia.Styling.Activators { @@ -11,47 +9,45 @@ namespace Avalonia.Styling.Activators internal class AndActivator : StyleActivatorBase, IStyleActivatorSink { private List? _sources; - private ulong _flags; - private ulong _mask; public int Count => _sources?.Count ?? 0; public void Add(IStyleActivator activator) { + if (IsSubscribed) + throw new AvaloniaInternalException("AndActivator is already subscribed."); _sources ??= new List(); _sources.Add(activator); } - void IStyleActivatorSink.OnNext(bool value, int tag) + void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive(); + + protected override bool EvaluateIsActive() { - if (value) - { - _flags |= 1ul << tag; - } - else - { - _flags &= ~(1ul << tag); - } + if (_sources is null || _sources.Count == 0) + return true; - if (_mask != 0) + var count = _sources.Count; + var mask = (1ul << count) - 1; + var flags = 0UL; + + for (var i = 0; i < count; ++i) { - PublishNext(_flags == _mask); + if (_sources[i].GetIsActive()) + flags |= 1ul << i; } + + return flags == mask; } protected override void Initialize() { if (_sources is object) { - var i = 0; - foreach (var source in _sources) { - source.Subscribe(this, i++); + source.Subscribe(this); } - - _mask = (1ul << Count) - 1; - PublishNext(_flags == _mask); } } @@ -64,8 +60,6 @@ namespace Avalonia.Styling.Activators source.Unsubscribe(this); } } - - _mask = 0; } } } diff --git a/src/Avalonia.Base/Styling/Activators/IStyleActivator.cs b/src/Avalonia.Base/Styling/Activators/IStyleActivator.cs index ac7b8b3ef1..e07534237a 100644 --- a/src/Avalonia.Base/Styling/Activators/IStyleActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/IStyleActivator.cs @@ -9,21 +9,39 @@ namespace Avalonia.Styling.Activators /// /// A style activator is very similar to an `IObservable{bool}` but is optimized for the /// particular use-case of activating a style according to a selector. It differs from - /// an observable in two major ways: + /// an observable in three major ways: /// /// - Can only have a single subscription - /// - The subscription can have a tag associated with it, allowing a subscriber to index - /// into a list of subscriptions without having to allocate additional objects. + /// - The activation state can be re-evaluated at any time by calling + /// - No error or completion messages /// [Unstable] public interface IStyleActivator : IDisposable { + /// + /// Gets a value indicating whether the style is subscribed. + /// + bool IsSubscribed { get; } + + /// + /// Gets the current activation state. + /// + /// + /// This method should read directly from its inputs and not rely on any subscriptions + /// to fire in order to be up-to-date. If a change in active state occurs when reading + /// this method then any subscribed should not be + /// notified of the change. + /// + bool GetIsActive(); + /// /// Subscribes to the activator. /// /// The listener. - /// An optional tag. - void Subscribe(IStyleActivatorSink sink, int tag = 0); + /// + /// This method should not call . + /// + void Subscribe(IStyleActivatorSink sink); /// /// Unsubscribes from the activator. diff --git a/src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs b/src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs index fbb18dc304..142a3c3517 100644 --- a/src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs +++ b/src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs @@ -12,7 +12,6 @@ namespace Avalonia.Styling.Activators /// Called when the subscribed activator value changes. /// /// The new value. - /// The subscription tag. - void OnNext(bool value, int tag); + void OnNext(bool value); } } diff --git a/src/Avalonia.Base/Styling/Activators/NotActivator.cs b/src/Avalonia.Base/Styling/Activators/NotActivator.cs index 1bb6ed3cd2..83f026e479 100644 --- a/src/Avalonia.Base/Styling/Activators/NotActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/NotActivator.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Avalonia.Styling.Activators +namespace Avalonia.Styling.Activators { /// /// An which inverts the state of an input activator. @@ -9,8 +7,9 @@ namespace Avalonia.Styling.Activators { private readonly IStyleActivator _source; public NotActivator(IStyleActivator source) => _source = source; - void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value); - protected override void Initialize() => _source.Subscribe(this, 0); + void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive(); + protected override bool EvaluateIsActive() => !_source.GetIsActive(); + protected override void Initialize() => _source.Subscribe(this); protected override void Deinitialize() => _source.Unsubscribe(this); } } diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs index 6f54cd5904..33d4cd0824 100644 --- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs @@ -26,31 +26,26 @@ namespace Avalonia.Styling.Activators _reversed = reversed; } - protected override void Initialize() + protected override bool EvaluateIsActive() { - PublishNext(IsMatching()); - _provider.ChildIndexChanged += ChildIndexChanged; + return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; } - protected override void Deinitialize() - { - _provider.ChildIndexChanged -= ChildIndexChanged; - } + protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged; + protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged; private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) { // Run matching again if: // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index. - // 2. e.Child is null, when all children indeces were changed. + // 2. e.Child is null, when all children indices were changed. // 3. Subscribed child index was changed. if (_reversed - || e.Child is null + || e.Child is null || e.Child == _control) { - PublishNext(IsMatching()); + ReevaluateIsActive(); } } - - private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; } } diff --git a/src/Avalonia.Base/Styling/Activators/OrActivator.cs b/src/Avalonia.Base/Styling/Activators/OrActivator.cs index fcb7d71e60..056731695a 100644 --- a/src/Avalonia.Base/Styling/Activators/OrActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/OrActivator.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Collections.Generic; +using System.Collections.Generic; namespace Avalonia.Styling.Activators { @@ -11,8 +9,6 @@ namespace Avalonia.Styling.Activators internal class OrActivator : StyleActivatorBase, IStyleActivatorSink { private List? _sources; - private ulong _flags; - private bool _initializing; public int Count => _sources?.Count ?? 0; @@ -22,38 +18,30 @@ namespace Avalonia.Styling.Activators _sources.Add(activator); } - void IStyleActivatorSink.OnNext(bool value, int tag) + void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive(); + + protected override bool EvaluateIsActive() { - if (value) - { - _flags |= 1ul << tag; - } - else - { - _flags &= ~(1ul << tag); - } + if (_sources is null || _sources.Count == 0) + return true; - if (!_initializing) + foreach (var source in _sources) { - PublishNext(_flags != 0); + if (source.GetIsActive()) + return true; } + + return false; } protected override void Initialize() { if (_sources is object) { - var i = 0; - - _initializing = true; - foreach (var source in _sources) { - source.Subscribe(this, i++); + source.Subscribe(this); } - - _initializing = false; - PublishNext(_flags != 0); } } diff --git a/src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs index 69de665485..388671f3f7 100644 --- a/src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs @@ -1,7 +1,5 @@ using System; -#nullable enable - namespace Avalonia.Styling.Activators { /// @@ -24,6 +22,12 @@ namespace Avalonia.Styling.Activators _value = value; } + protected override bool EvaluateIsActive() + { + var value = _control.GetValue(_property); + return PropertyEqualsSelector.Compare(_property.PropertyType, value, _value); + } + protected override void Initialize() { _subscription = _control.GetObservable(_property).Subscribe(this); @@ -33,6 +37,6 @@ namespace Avalonia.Styling.Activators void IObserver.OnCompleted() { } void IObserver.OnError(Exception error) { } - void IObserver.OnNext(object? value) => PublishNext(PropertyEqualsSelector.Compare(_property.PropertyType, value, _value)); + void IObserver.OnNext(object? value) => ReevaluateIsActive(); } } diff --git a/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs b/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs index 578098b2b0..06848a7524 100644 --- a/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs +++ b/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Avalonia.Styling.Activators { /// @@ -8,26 +6,30 @@ namespace Avalonia.Styling.Activators internal abstract class StyleActivatorBase : IStyleActivator { private IStyleActivatorSink? _sink; - private int _tag; - private bool? _value; + private bool _value; + + public bool GetIsActive() => _value = EvaluateIsActive(); + + public bool IsSubscribed => _sink is not null; - public void Subscribe(IStyleActivatorSink sink, int tag = 0) + public void Subscribe(IStyleActivatorSink sink) { if (_sink is null) { - _sink = sink; - _tag = tag; - _value = null; Initialize(); + _sink = sink; } else { - throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once."); + throw new AvaloniaInternalException("StyleActivator is already subscribed."); } } public void Unsubscribe(IStyleActivatorSink sink) { + if (_sink is null) + return; + if (_sink != sink) { throw new AvaloniaInternalException("StyleActivatorSink is not subscribed."); @@ -37,22 +39,51 @@ namespace Avalonia.Styling.Activators Deinitialize(); } - public void PublishNext(bool value) + public void Dispose() { - if (_value != value) + _sink = null; + Deinitialize(); + } + + /// + /// Evaluates the activation state. + /// + /// + /// This method should read directly from its inputs and not rely on any subscriptions to + /// fire in order to be up-to-date. + /// + protected abstract bool EvaluateIsActive(); + + /// + /// Called from a derived class when the activation state should be re-evaluated and the + /// subscriber notified of any change. + /// + /// + /// The evaluated active state; + /// + protected bool ReevaluateIsActive() + { + var value = EvaluateIsActive(); + + if (value != _value) { _value = value; - _sink?.OnNext(value, _tag); + _sink?.OnNext(value); } - } - public void Dispose() - { - _sink = null; - Deinitialize(); + return value; } + /// + /// Called in response to a to allow the + /// derived class to set up any necessary subscriptions. + /// protected abstract void Initialize(); + + /// + /// Called in response to an or + /// to allow the derived class to dispose any active subscriptions. + /// protected abstract void Deinitialize(); } } diff --git a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs index 3f70ff50b3..faeacfe0f9 100644 --- a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs @@ -1,10 +1,6 @@ using System.Collections.Generic; -using System.Collections.Specialized; -using Avalonia.Collections; using Avalonia.Controls; -#nullable enable - namespace Avalonia.Styling.Activators { /// @@ -52,22 +48,9 @@ namespace Avalonia.Styling.Activators return remainingMatches == 0; } - void IClassesChangedListener.Changed() - { - PublishNext(IsMatching()); - } - - protected override void Initialize() - { - PublishNext(IsMatching()); - _classes.AddListener(this); - } - - protected override void Deinitialize() - { - _classes.RemoveListener(this); - } - - private bool IsMatching() => AreClassesMatching(_classes, _match); + void IClassesChangedListener.Changed() => ReevaluateIsActive(); + protected override bool EvaluateIsActive() => AreClassesMatching(_classes, _match); + protected override void Initialize() => _classes.AddListener(this); + protected override void Deinitialize() => _classes.RemoveListener(this); } } diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index 9512dc34df..a6093bf026 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -27,11 +27,11 @@ namespace Avalonia.Styling /// public override Type? TargetType => null; - public override string ToString() + public override string ToString(Style? owner) { if (_selectorString == null) { - _selectorString = _parent.ToString() + " > "; + _selectorString = _parent.ToString(owner) + " > "; } return _selectorString; diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 644e8b32d4..46a3267f70 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.PropertyStore; namespace Avalonia.Styling { @@ -51,13 +52,7 @@ namespace Avalonia.Styling return result; } - public override string ToString() - { - if (TargetType is not null) - return "ControlTheme: " + TargetType.Name; - else - return "ControlTheme"; - } + public override string ToString() => TargetType?.Name ?? "ControlTheme"; internal override void SetParent(StyleBase? parent) { diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 677a924189..728dd08301 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -25,11 +25,11 @@ namespace Avalonia.Styling /// public override Type? TargetType => null; - public override string ToString() + public override string ToString(Style? owner) { if (_selectorString == null) { - _selectorString = _parent.ToString() + ' '; + _selectorString = _parent.ToString(owner) + ' '; } return _selectorString; diff --git a/src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs b/src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs new file mode 100644 index 0000000000..a95835b7fc --- /dev/null +++ b/src/Avalonia.Base/Styling/DirectPropertySetterBindingInstance.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Styling +{ + internal class DirectPropertySetterBindingInstance : ISetterInstance + { + } +} diff --git a/src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs b/src/Avalonia.Base/Styling/DirectPropertySetterInstance.cs new file mode 100644 index 0000000000..1707be454f --- /dev/null +++ b/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 + { + } +} diff --git a/src/Avalonia.Base/Styling/ISetter.cs b/src/Avalonia.Base/Styling/ISetter.cs index 71ae5d84c0..87ba6c5680 100644 --- a/src/Avalonia.Base/Styling/ISetter.cs +++ b/src/Avalonia.Base/Styling/ISetter.cs @@ -12,13 +12,13 @@ namespace Avalonia.Styling /// /// Instances a setter on a control. /// + /// The style which contains the setter. /// The control. /// An . /// /// This method should return an which can be used to apply - /// the setter to the specified control. Note that it should not apply the setter value - /// until is called. + /// the setter to the specified control. /// - ISetterInstance Instance(IStyleable target); + ISetterInstance Instance(IStyleInstance styleInstance, IStyleable target); } } diff --git a/src/Avalonia.Base/Styling/ISetterInstance.cs b/src/Avalonia.Base/Styling/ISetterInstance.cs index e0d3137619..4a65d6deeb 100644 --- a/src/Avalonia.Base/Styling/ISetterInstance.cs +++ b/src/Avalonia.Base/Styling/ISetterInstance.cs @@ -1,40 +1,12 @@ -using System; -using Avalonia.Metadata; +using Avalonia.Metadata; namespace Avalonia.Styling { /// - /// Represents a setter that has been instanced on a control. + /// Represents an that has been instanced on a control. /// [Unstable] - public interface ISetterInstance : IDisposable + public interface ISetterInstance { - /// - /// Starts the setter instance. - /// - /// Whether the parent style has an activator. - /// - /// If is false then the setter should be immediately - /// applied and and should not be called. - /// If true, then bindings etc should be initiated but not produce a value until - /// called. - /// - public void Start(bool hasActivator); - - /// - /// Activates the setter. - /// - /// - /// Should only be called if hasActivator was true when was called. - /// - public void Activate(); - - /// - /// Deactivates the setter. - /// - /// - /// Should only be called if hasActivator was true when was called. - /// - public void Deactivate(); } } diff --git a/src/Avalonia.Base/Styling/IStyleInstance.cs b/src/Avalonia.Base/Styling/IStyleInstance.cs index 262f336e05..749a2c84d5 100644 --- a/src/Avalonia.Base/Styling/IStyleInstance.cs +++ b/src/Avalonia.Base/Styling/IStyleInstance.cs @@ -1,13 +1,12 @@ -using System; -using Avalonia.Metadata; +using Avalonia.Metadata; namespace Avalonia.Styling { /// - /// Represents a style that has been instanced on a control. + /// Represents a that has been instanced on a control. /// [Unstable] - public interface IStyleInstance : IDisposable + public interface IStyleInstance { /// /// Gets the source style. @@ -15,18 +14,16 @@ namespace Avalonia.Styling IStyle Source { get; } /// - /// Gets a value indicating whether this style has an activator. + /// Gets a value indicating whether this style instance has an activator. /// + /// + /// A style instance without an activator will always be active. + /// bool HasActivator { get; } - + /// /// Gets a value indicating whether this style is active. /// bool IsActive { get; } - - /// - /// Instructs the style to start acting upon the control. - /// - void Start(); } } diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs index 254da4d85c..e94fc5c4e6 100644 --- a/src/Avalonia.Base/Styling/IStyleable.cs +++ b/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 /// ControlTheme? GetEffectiveTheme(); - /// - /// Notifies the element that a style has been applied. - /// - /// The style instance. - void StyleApplied(IStyleInstance instance); - - /// - /// Detaches all styles applied to the element. - /// void DetachStyles(); - - /// - /// Detaches a collection of styles, if applied to the element. - /// - void DetachStyles(IReadOnlyList styles); - - /// - /// Detaches all styles from the element and queues a restyle. - /// - void InvalidateStyles(); } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 77c5b719c6..dd627a7db9 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -11,7 +11,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; public override Type? TargetType => null; - public override string ToString() => "^"; + public override string ToString(Style? owner) => owner?.Parent?.ToString() ?? "^"; protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index c7727bb6b8..4491762b8f 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -35,11 +35,11 @@ namespace Avalonia.Styling public override Type? TargetType => _previous?.TargetType; /// - public override string ToString() + public override string ToString(Style? owner) { if (_selectorString == null) { - _selectorString = $"{_previous?.ToString()}:not({_argument})"; + _selectorString = $"{_previous?.ToString(owner)}:not({_argument})"; } return _selectorString; diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index c872a40ad4..9191d2eaa5 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -107,11 +107,11 @@ namespace Avalonia.Styling protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; - public override string ToString() + public override string ToString(Style? owner) { var expectedCapacity = NthLastChildSelectorName.Length + 8; var stringBuilder = StringBuilderCache.Acquire(expectedCapacity); - stringBuilder.Append(_previous?.ToString()); + stringBuilder.Append(_previous?.ToString(owner)); stringBuilder.Append(':'); stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName); diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index af9249864f..0c48b76ded 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Styling.Activators; #nullable enable @@ -55,11 +56,11 @@ namespace Avalonia.Styling } /// - public override string ToString() + public override string ToString(Style? owner) { if (_selectorString == null) { - _selectorString = string.Join(", ", _selectors); + _selectorString = string.Join(", ", _selectors.Select(x => x.ToString(owner))); } return _selectorString; diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index e98ff3f9c9..7d58bab4a3 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -38,7 +38,7 @@ namespace Avalonia.Styling public override Type? TargetType => _previous?.TargetType; /// - public override string ToString() + public override string ToString(Style? owner) { if (_selectorString == null) { @@ -46,7 +46,7 @@ namespace Avalonia.Styling if (_previous != null) { - builder.Append(_previous.ToString()); + builder.Append(_previous.ToString(owner)); } builder.Append('['); diff --git a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs index edd3fb7d48..826b45582d 100644 --- a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs @@ -1,200 +1,60 @@ using System; -using System.Reactive.Subjects; using Avalonia.Data; -using Avalonia.Reactive; - -#nullable enable +using Avalonia.PropertyStore; namespace Avalonia.Styling { - /// - /// A which has been instanced on a control and has an - /// as its value. - /// - /// The target property type. - internal class PropertySetterBindingInstance : SingleSubscriberObservableBase>, - ISubject>, - ISetterInstance + internal class PropertySetterBindingInstance : UntypedBindingEntry, ISetterInstance { - private readonly IStyleable _target; - private readonly StyledPropertyBase? _styledProperty; - private readonly DirectPropertyBase? _directProperty; - private readonly InstancedBinding? _binding; - private readonly Inner _inner; - private BindingValue _value; - private IDisposable? _subscription; - private IDisposable? _subscriptionTwoWay; - private IDisposable? _innerSubscription; - private bool _isActive; + private readonly AvaloniaObject _target; + private readonly BindingMode _mode; public PropertySetterBindingInstance( - IStyleable target, - StyledPropertyBase property, - IBinding binding) + AvaloniaObject target, + StyleInstance instance, + AvaloniaProperty property, + BindingMode mode, + IObservable source) + : base(instance, property, source) { _target = target; - _styledProperty = property; - _binding = binding.Initiate(_target, property); + _mode = mode; - if (_binding?.Mode == BindingMode.OneTime) + if (mode == BindingMode.TwoWay && + source is not IObserver) { - // For the moment, we don't support OneTime bindings in setters, because I'm not - // sure what the semantics should be in the case of activation/deactivation. - throw new NotSupportedException("OneTime bindings are not supported in setters."); + throw new NotSupportedException( + "Attempting to bind two-way with a binding source which doesn't support it."); } - - _inner = new Inner(this); } - public PropertySetterBindingInstance( - IStyleable target, - DirectPropertyBase property, - IBinding binding) + public override void Unsubscribe() { - _target = target; - _directProperty = property; - _binding = binding.Initiate(_target, property); - _inner = new Inner(this); + _target.PropertyChanged -= PropertyChanged; + base.Unsubscribe(); } - public void Start(bool hasActivator) + protected override void Start(bool produceValue) { - if (_binding is null) - return; - - _isActive = !hasActivator; - - if (_styledProperty is object) + if (!IsSubscribed) { - if (_binding.Mode != BindingMode.OneWayToSource) + if (_mode == BindingMode.TwoWay) { - var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; - _subscription = _target.Bind(_styledProperty, this, priority); + var observer = (IObserver)Source; + _target.PropertyChanged += PropertyChanged; } - if (_binding.Mode == BindingMode.TwoWay) - { - _subscriptionTwoWay = _target.GetBindingObservable(_styledProperty).Subscribe(this); - } - } - else - { - if (_binding.Mode != BindingMode.OneWayToSource) - { - _subscription = _target.Bind(_directProperty!, this); - } - - if (_binding.Mode == BindingMode.TwoWay) - { - _subscriptionTwoWay = _target.GetBindingObservable(_directProperty!).Subscribe(this); - } + base.Start(produceValue); } } - public void Activate() + private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { - if (_binding is null) - return; - - if (!_isActive) + if (e.Property == Property && e.Priority >= BindingPriority.LocalValue) { - _innerSubscription ??= _binding.Observable!.Subscribe(_inner); - _isActive = true; - PublishNext(); + if (Frame.Owner is not null && !Frame.Owner.IsEvaluating) + ((IObserver)Source).OnNext(e.NewValue); } } - - public void Deactivate() - { - if (_isActive) - { - _isActive = false; - _innerSubscription?.Dispose(); - _innerSubscription = null; - PublishNext(); - } - } - - public override void Dispose() - { - if (_subscription is object) - { - var sub = _subscription; - _subscription = null; - sub.Dispose(); - } - - if (_subscriptionTwoWay is object) - { - var sub = _subscriptionTwoWay; - _subscriptionTwoWay = null; - sub.Dispose(); - } - - base.Dispose(); - } - - void IObserver>.OnCompleted() - { - // This is the observable coming from the target control. It should not complete. - } - - void IObserver>.OnError(Exception error) - { - // This is the observable coming from the target control. It should not error. - } - - void IObserver>.OnNext(BindingValue value) - { - if (value.HasValue && _isActive && _binding?.Subject is not null) - { - _binding.Subject.OnNext(value.Value); - } - } - - protected override void Subscribed() - { - if (_isActive && _binding?.Observable is not null) - { - if (_innerSubscription is null) - { - _innerSubscription ??= _binding.Observable!.Subscribe(_inner); - } - else - { - PublishNext(); - } - } - } - - protected override void Unsubscribed() - { - _innerSubscription?.Dispose(); - _innerSubscription = null; - } - - private void PublishNext() - { - PublishNext(_isActive ? _value : default); - } - - private void ConvertAndPublishNext(object? value) - { - _value = BindingValue.FromUntyped(value); - - if (_isActive) - { - PublishNext(); - } - } - - private class Inner : IObserver - { - private readonly PropertySetterBindingInstance _owner; - public Inner(PropertySetterBindingInstance owner) => _owner = owner; - public void OnCompleted() => _owner.PublishCompleted(); - public void OnError(Exception error) => _owner.PublishError(error); - public void OnNext(object? value) => _owner.ConvertAndPublishNext(value); - } } } diff --git a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs index 0f6efef1be..7a39407ba2 100644 --- a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs @@ -1,127 +1,24 @@ using System; -using Avalonia.Data; -using Avalonia.Reactive; - -#nullable enable +using Avalonia.PropertyStore; namespace Avalonia.Styling { - /// - /// A which has been instanced on a control and whose value is lazily - /// evaluated. - /// - /// The target property type. - internal class PropertySetterTemplateInstance : SingleSubscriberObservableBase>, - ISetterInstance + internal class PropertySetterTemplateInstance : IValueEntry, ISetterInstance { - private readonly IStyleable _target; - private readonly StyledPropertyBase? _styledProperty; - private readonly DirectPropertyBase? _directProperty; private readonly ITemplate _template; - private BindingValue _value; - private IDisposable? _subscription; - private bool _isActive; - - public PropertySetterTemplateInstance( - IStyleable target, - StyledPropertyBase property, - ITemplate template) - { - _target = target; - _styledProperty = property; - _template = template; - } + private object? _value; - public PropertySetterTemplateInstance( - IStyleable target, - DirectPropertyBase property, - ITemplate template) + public PropertySetterTemplateInstance(AvaloniaProperty property, ITemplate template) { - _target = target; - _directProperty = property; _template = template; + Property = property; } - public void Start(bool hasActivator) - { - _isActive = !hasActivator; - - if (_styledProperty is not null) - { - var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; - _subscription = _target.Bind(_styledProperty, this, priority); - } - else - { - _subscription = _target.Bind(_directProperty!, this); - } - } - - public void Activate() - { - if (!_isActive) - { - _isActive = true; - PublishNext(); - } - } - - public void Deactivate() - { - if (_isActive) - { - _isActive = false; - PublishNext(); - } - } - - public override void Dispose() - { - if (_subscription is not null) - { - var sub = _subscription; - _subscription = null; - sub.Dispose(); - } - else if (_isActive) - { - if (_styledProperty is not null) - { - _target.ClearValue(_styledProperty); - } - else - { - _target.ClearValue(_directProperty!); - } - } - - base.Dispose(); - } + public bool HasValue => true; + public AvaloniaProperty Property { get; } - protected override void Subscribed() => PublishNext(); - protected override void Unsubscribed() { } + public object? GetValue() => _value ??= _template.Build(); - private void EnsureTemplate() - { - if (_value.HasValue) - { - return; - } - - _value = (T) _template.Build(); - } - - private void PublishNext() - { - if (_isActive) - { - EnsureTemplate(); - PublishNext(_value); - } - else - { - PublishNext(default); - } - } + void IValueEntry.Unsubscribe() { } } } diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 7ce17518dd..7f5ee48351 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -67,6 +67,15 @@ namespace Avalonia.Styling return match; } + public override string ToString() => ToString(null); + + /// + /// Gets a string representing the selector, with the nesting separator (`^`) replaced with + /// the parent selector. + /// + /// The owner style. + public abstract string ToString(Style? owner); + /// /// Evaluates the selector for a match. /// diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index d989bb0706..92b35c9300 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/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 is used to set a value on a /// depending on a condition. /// - public class Setter : ISetter, IAnimationSetter + public class Setter : ISetter, IValueEntry, ISetterInstance, IAnimationSetter { private object? _value; + private DirectPropertySetterInstance? _direct; /// /// Initializes a new instance of the class. @@ -30,7 +30,7 @@ namespace Avalonia.Styling /// /// The property to set. /// The property value. - public Setter(AvaloniaProperty property, object value) + public Setter(AvaloniaProperty property, object? value) { Property = property; Value = value; @@ -57,16 +57,72 @@ namespace Avalonia.Styling } } - public ISetterInstance Instance(IStyleable target) - { - target = target ?? throw new ArgumentNullException(nameof(target)); + bool IValueEntry.HasValue => true; + AvaloniaProperty IValueEntry.Property => EnsureProperty(); + + public override string ToString() => $"Setter: {Property} = {Value}"; + void IValueEntry.Unsubscribe() { } + + ISetterInstance ISetter.Instance(IStyleInstance instance, IStyleable target) + { + if (target is not AvaloniaObject ao) + throw new InvalidOperationException("Don't know how to instance a style on this type."); if (Property is null) - { throw new InvalidOperationException("Setter.Property must be set."); + if (Property.IsDirect && instance.HasActivator) + throw new InvalidOperationException( + $"Cannot set direct property '{Property}' in '{instance.Source}' because the style has an activator."); + + if (Value is IBinding binding) + return SetBinding((StyleInstance)instance, ao, binding); + else if (Value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(Property.PropertyType)) + return new PropertySetterTemplateInstance(Property, template); + else if (!Property.IsValidValue(Value)) + throw new InvalidCastException($"Setter value '{Value}' is not a valid value for property '{Property}'."); + else if (Property.IsDirect) + return SetDirectValue(target); + else + return this; + } + + object? IValueEntry.GetValue() => Value; + + private AvaloniaProperty EnsureProperty() + { + return Property ?? throw new InvalidOperationException("Setter.Property must be set."); + } + + private ISetterInstance SetBinding(StyleInstance instance, AvaloniaObject target, IBinding binding) + { + if (!Property!.IsDirect) + { + var i = binding.Initiate(target, Property)!; + var mode = i.Mode; + + if (mode == BindingMode.Default) + { + mode = Property!.GetMetadata(target.GetType()).DefaultBindingMode; + } + + if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) + { + return new PropertySetterBindingInstance(target, instance, Property, mode, i.Observable!); + } + + throw new NotSupportedException(); + } + else + { + target.Bind(Property, binding); + return new DirectPropertySetterBindingInstance(); } + } - return Property.CreateSetterInstance(target, Value); + private ISetterInstance SetDirectValue(IStyleable target) + { + target.SetValue(Property!, Value); + return _direct ??= new DirectPropertySetterInstance(); } } } diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index c61b08b2a1..913c437bc4 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.PropertyStore; namespace Avalonia.Styling { @@ -48,7 +49,9 @@ namespace Avalonia.Styling SelectorMatch.NeverThisInstance); if (match.IsMatch) + { Attach(target, match.Activator); + } result = match.Result; } @@ -65,17 +68,7 @@ namespace Avalonia.Styling /// Returns a string representation of the style. /// /// A string representation of the style. - public override string ToString() - { - if (Selector != null) - { - return "Style: " + Selector.ToString(); - } - else - { - return "Style"; - } - } + public override string ToString() => Selector?.ToString(this) ?? "Style"; internal override void SetParent(StyleBase? parent) { diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index 306a4cf010..c914fbf8cc 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; +using Avalonia.PropertyStore; using Avalonia.Styling.Activators; namespace Avalonia.Styling @@ -18,6 +19,7 @@ namespace Avalonia.Styling private List? _setters; private List? _animations; private StyleCache? _childCache; + private StyleInstance? _sharedInstance; public IList Children => _children ??= new(this); @@ -80,11 +82,46 @@ namespace Avalonia.Styling return _resources?.TryGetResource(key, out result) ?? false; } - internal void Attach(IStyleable target, IStyleActivator? activator) + internal ValueFrame Attach(IStyleable target, IStyleActivator? activator) { - var instance = new StyleInstance(this, target, _setters, _animations, activator); - target.StyleApplied(instance); - instance.Start(); + if (target is not AvaloniaObject ao) + throw new InvalidOperationException("Styles can only be applied to AvaloniaObjects."); + + StyleInstance instance; + + if (_sharedInstance is not null) + { + instance = _sharedInstance; + } + else + { + var canShareInstance = activator is null; + + instance = new StyleInstance(this, activator); + + if (_setters is not null) + { + foreach (var setter in _setters) + { + var setterInstance = setter.Instance(instance, target); + instance.Add(setterInstance); + canShareInstance &= setterInstance == setter; + } + } + + if (_animations is not null) + instance.Add(_animations); + + if (canShareInstance) + { + instance.MakeShared(); + _sharedInstance = instance; + } + } + + ao.GetValueStore().AddFrame(instance); + instance.ApplyAnimations(ao); + return instance; } internal SelectorMatchResult TryAttachChildren(IStyleable target, object? host) diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs index db96da6821..2d7c695b32 100644 --- a/src/Avalonia.Base/Styling/StyleInstance.cs +++ b/src/Avalonia.Base/Styling/StyleInstance.cs @@ -1,137 +1,103 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reactive.Subjects; using Avalonia.Animation; +using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Styling.Activators; -#nullable enable - namespace Avalonia.Styling { /// - /// A which has been instanced on a control. + /// Stores state for a that has been instanced on a control. /// - internal sealed class StyleInstance : IStyleInstance, IStyleActivatorSink + /// + /// is based on meaning that it is + /// injected directly into the value store of an . 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. + /// + internal class StyleInstance : ValueFrame, IStyleInstance, IStyleActivatorSink, IDisposable { - private readonly ISetterInstance[]? _setters; - private readonly IDisposable[]? _animations; private readonly IStyleActivator? _activator; - private readonly Subject? _animationTrigger; + private bool _isActive; + private List? _setters; + private List? _animations; + private Subject? _animationTrigger; - public StyleInstance( - IStyle source, - IStyleable target, - IReadOnlyList? setters, - IReadOnlyList? animations, - IStyleActivator? activator = null) + public StyleInstance(IStyle style, IStyleActivator? activator) { - Source = source ?? throw new ArgumentNullException(nameof(source)); - Target = target ?? throw new ArgumentNullException(nameof(target)); _activator = activator; - IsActive = _activator is null; - - if (setters is not null) - { - var setterCount = setters.Count; - - _setters = new ISetterInstance[setterCount]; - - for (var i = 0; i < setterCount; ++i) - { - _setters[i] = setters[i].Instance(Target); - } - } - - if (animations is not null && target is Animatable animatable) - { - var animationsCount = animations.Count; - - _animations = new IDisposable[animationsCount]; - _animationTrigger = new Subject(); - - for (var i = 0; i < animationsCount; ++i) - { - _animations[i] = animations[i].Apply(animatable, null, _animationTrigger); - } - } + Priority = activator is object ? BindingPriority.StyleTrigger : BindingPriority.Style; + Source = style; } - public bool HasActivator => _activator is not null; - public bool IsActive { get; private set; } + public bool HasActivator => _activator is object; + public IStyle Source { get; } - public IStyleable Target { get; } - public void Start() + bool IStyleInstance.IsActive => _isActive; + + public void Add(ISetterInstance instance) { - var hasActivator = HasActivator; - - if (_setters is not null) - { - foreach (var setter in _setters) - { - setter.Start(hasActivator); - } - } - - if (hasActivator) + if (instance is IValueEntry valueEntry) { - _activator!.Subscribe(this, 0); - } - else if (_animationTrigger is not null) - { - _animationTrigger.OnNext(true); + if (Contains(valueEntry.Property)) + throw new InvalidOperationException( + $"Duplicate setter encountered for property '{valueEntry.Property}' in '{Source}'."); + Add(valueEntry); } + else + (_setters ??= new()).Add(instance); } - public void Dispose() + public void Add(IList animations) { - if (_setters is not null) - { - foreach (var setter in _setters) - { - setter.Dispose(); - } - } + if (_animations is null) + _animations = new List(animations); + else + _animations.AddRange(animations); + } - if (_animations is not null) + public void ApplyAnimations(AvaloniaObject control) + { + if (_animations is not null && control is Animatable animatable) { - foreach (var subscription in _animations) - { - subscription.Dispose(); - } + _animationTrigger ??= new Subject(); + foreach (var animation in _animations) + animation.Apply(animatable, null, _animationTrigger); } + } + public override void Dispose() + { + base.Dispose(); _activator?.Dispose(); } - private void ActivatorChanged(bool value) + public new void MakeShared() => base.MakeShared(); + + void IStyleActivatorSink.OnNext(bool value) { - if (IsActive != value) - { - IsActive = value; + Owner?.OnFrameActivationChanged(this); + _animationTrigger?.OnNext(value); + } - _animationTrigger?.OnNext(value); + protected override bool GetIsActive(out bool hasChanged) + { + var previous = _isActive; - if (_setters is not null) - { - if (IsActive) - { - foreach (var setter in _setters) - { - setter.Activate(); - } - } - else - { - foreach (var setter in _setters) - { - setter.Deactivate(); - } - } - } + if (_activator?.IsSubscribed == false) + { + _activator.Subscribe(this); + _animationTrigger?.OnNext(_activator.GetIsActive()); } - } - void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value); + _isActive = _activator?.GetIsActive() ?? true; + hasChanged = _isActive != previous; + return _isActive; + } } } diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index 278e24a203..82d934ee34 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -26,11 +26,11 @@ namespace Avalonia.Styling /// public override Type? TargetType => null; - public override string ToString() + public override string ToString(Style? owner) { if (_selectorString == null) { - _selectorString = _parent.ToString() + " /template/ "; + _selectorString = _parent.ToString(owner) + " /template/ "; } return _selectorString; diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 1833f0d133..4e225985d2 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -84,11 +84,11 @@ namespace Avalonia.Styling public IList Classes => _classes.Value; /// - public override string ToString() + public override string ToString(Style? owner) { if (_selectorString == null) { - _selectorString = BuildSelectorString(); + _selectorString = BuildSelectorString(owner); } return _selectorString; @@ -143,13 +143,13 @@ namespace Avalonia.Styling protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; - private string BuildSelectorString() + private string BuildSelectorString(Style? owner) { var builder = StringBuilderCache.Acquire(); if (_previous != null) { - builder.Append(_previous.ToString()); + builder.Append(_previous.ToString(owner)); } if (TargetType != null) diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs new file mode 100644 index 0000000000..5cac2ef658 --- /dev/null +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Avalonia.Utilities +{ + /// + /// Stores values with as key. + /// + /// Stored value type. + /// + /// This struct implements the most commonly-used part of the dictionary API, but does + /// not implement . In particular, this struct + /// is not enumerable. Enumeration is intended to be done by index for better performance. + /// + internal struct AvaloniaPropertyDictionary + { + private const int DefaultInitialCapacity = 4; + private Entry[]? _entries; + private int _entryCount; + + /// + /// Initializes a new instance of the + /// class that is empty and has the default initial capacity. + /// + public AvaloniaPropertyDictionary() + { + _entries = null; + _entryCount = 0; + } + + /// + /// Initializes a new instance of the + /// class that is empty and has the specified initial capacity. + /// + /// + /// The initial number of elements that the collection can contain. + /// + public AvaloniaPropertyDictionary(int capactity) + { + _entries = new Entry[capactity]; + _entryCount = 0; + } + + /// + /// Gets the number of key/value pairs contained in the collection. + /// + public int Count => _entryCount; + + /// + /// Gets or sets the value associated with the specified key. + /// + /// The key to get or set. + /// + /// The value associated with the specified key. If the key is not found, a get operation + /// throws a , and a set operation creates a + /// new element for the specified key. + /// + /// + /// The key does not exist in the collection. + /// + public TValue this[AvaloniaProperty property] + { + get + { + if (!TryGetEntry(property.Id, out var index)) + ThrowNotFound(); + return _entries[index].Value; + } + set + { + if (TryGetEntry(property.Id, out var index)) + _entries[index] = new Entry(property, value); + else + InsertEntry(new Entry(property, value), index); + } + } + + /// + /// Gets the value at the specified index. + /// + /// + /// The index of the entry, between 0 and - 1. + /// + public TValue this[int index] + { + get + { + if (index >= _entryCount) + ThrowOutOfRange(); + return _entries![index].Value; + } + } + + /// + /// Adds the specified key and value to the dictionary. + /// + /// The key. + /// The value of the element to add. + public void Add(AvaloniaProperty property, TValue value) + { + if (TryGetEntry(property.Id, out var index)) + ThrowDuplicate(); + InsertEntry(new Entry(property, value), index); + } + + /// + /// Removes all keys and values from the collection. + /// + /// + /// The Count property is set to 0, and references to other objects from elements of the + /// collection are also released. The capacity remains unchanged. + /// + public void Clear() + { + if (_entries is not null) + { + Array.Clear(_entries, 0, _entries.Length); + _entryCount = 0; + } + } + + /// + /// Determines whether the collection contains the specified key. + /// + /// The key. + public bool ContainsKey(AvaloniaProperty property) => TryGetEntry(property.Id, out _); + + /// + /// Gets the key and value at the specified index. + /// + /// + /// The index of the entry, between 0 and - 1. + /// + /// + /// When this method returns, contains the key at the specified index. + /// + /// + /// When this method returns, contains the value at the specified index. + /// + public void GetKeyValue(int index, out AvaloniaProperty key, out TValue value) + { + if (index >= _entryCount) + ThrowOutOfRange(); + ref var entry = ref _entries![index]; + key = entry.Property; + value = entry.Value; + } + + /// + /// Removes the value of the specified key from the collection. + /// + /// The key. + /// + /// true if the element is successfully found and removed; otherwise, false. This method + /// returns false if key is not found in the collection. + /// + public bool Remove(AvaloniaProperty property) + { + if (TryGetEntry(property.Id, out var index)) + { + RemoveAt(index); + return true; + } + + return false; + } + + /// + /// Removes the value of the specified key from the collection, and copies the element to + /// the value parameter. + /// + /// The key. + /// The removed element. + /// + /// true if the element is successfully found and removed; otherwise, false. This method + /// returns false if key is not found in the collection. + /// + public bool Remove(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) + { + if (TryGetEntry(property.Id, out var index)) + { + value = _entries[index].Value; + RemoveAt(index); + return true; + } + + value = default; + return false; + } + + /// + /// Removes the element at the specified index from the collection. + /// + /// The index. + public void RemoveAt(int index) + { + if (_entries is null) + throw new IndexOutOfRangeException(); + + Array.Copy(_entries, index + 1, _entries, index, _entryCount - index - 1); + _entryCount--; + _entries[_entryCount] = default; + } + + /// + /// Attempts to add the specified key and value to the collection. + /// + /// The key. + /// The value of the element to add. + /// + public bool TryAdd(AvaloniaProperty property, TValue value) + { + if (TryGetEntry(property.Id, out var index)) + return false; + InsertEntry(new Entry(property, value), index); + return true; + } + + /// + /// Gets the value associated with the specified key. + /// + /// The property key. + /// + /// When this method returns, contains the value associated with the specified key, + /// if the property is found; otherwise, null. This parameter is passed uninitialized. + /// + /// + public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) + { + if (TryGetEntry(property.Id, out var index)) + { + value = _entries[index].Value; + return true; + } + + value = default; + return false; + } + + [MemberNotNullWhen(true, nameof(_entries))] + private bool TryGetEntry(int propertyId, out int index) + { + int checkIndex; + int iLo = 0; + int iHi = _entryCount; + + if (iHi <= 0) + { + index = 0; + return false; + } + + // Do a binary search to find the value + while (iHi - iLo > 3) + { + int iPv = (iHi + iLo) / 2; + checkIndex = _entries![iPv].Property.Id; + + if (propertyId == checkIndex) + { + index = iPv; + return true; + } + + if (propertyId <= checkIndex) + { + iHi = iPv; + } + else + { + iLo = iPv + 1; + } + } + + // Now we only have three values to search; switch to a linear search + do + { + checkIndex = _entries![iLo].Property.Id; + + if (checkIndex == propertyId) + { + index = iLo; + return true; + } + + if (checkIndex > propertyId) + { + // we've gone past the targetIndex - return not found + break; + } + + iLo++; + } while (iLo < iHi); + + index = iLo; + return false; + } + + [MemberNotNull(nameof(_entries))] + private void InsertEntry(Entry entry, int entryIndex) + { + if (_entryCount > 0) + { + if (_entryCount == _entries!.Length) + { + const double growthFactor = 1.2; + var newSize = (int)(_entryCount * growthFactor); + + if (newSize == _entryCount) + { + newSize++; + } + + var destEntries = new Entry[newSize]; + + Array.Copy(_entries, 0, destEntries, 0, entryIndex); + + destEntries[entryIndex] = entry; + + Array.Copy(_entries, entryIndex, destEntries, entryIndex + 1, _entryCount - entryIndex); + + _entries = destEntries; + } + else + { + Array.Copy( + _entries, + entryIndex, + _entries, + entryIndex + 1, + _entryCount - entryIndex); + + _entries[entryIndex] = entry; + } + } + else + { + _entries ??= new Entry[DefaultInitialCapacity]; + _entries[0] = entry; + } + + _entryCount++; + } + + [DoesNotReturn] + private static void ThrowOutOfRange() => throw new IndexOutOfRangeException(); + + [DoesNotReturn] + private static void ThrowDuplicate() => + throw new ArgumentException("An item with the same key has already been added."); + + [DoesNotReturn] + private static void ThrowNotFound() => throw new KeyNotFoundException(); + + private readonly struct Entry + { + public readonly AvaloniaProperty Property; + public readonly TValue Value; + + public Entry(AvaloniaProperty property, TValue value) + { + Property = property; + Value = value; + } + } + } +} diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs deleted file mode 100644 index cbe3771577..0000000000 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Avalonia.Utilities -{ - /// - /// Stores values with as key. - /// - /// Stored value type. - internal sealed class AvaloniaPropertyValueStore - { - // The last item in the list is always int.MaxValue. - private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } }; - - private Entry[] _entries; - - public AvaloniaPropertyValueStore() - { - _entries = s_emptyEntries; - } - - public int Count => _entries.Length - 1; - public TValue this[int index] => _entries[index].Value; - - private (int, bool) TryFindEntry(int propertyId) - { - if (_entries.Length <= 12) - { - // For small lists, we use an optimized linear search. Since the last item in the list - // is always int.MaxValue, we can skip a conditional branch in each iteration. - // By unrolling the loop, we can skip another unconditional branch in each iteration. - - if (_entries[0].PropertyId >= propertyId) - return (0, _entries[0].PropertyId == propertyId); - if (_entries[1].PropertyId >= propertyId) - return (1, _entries[1].PropertyId == propertyId); - if (_entries[2].PropertyId >= propertyId) - return (2, _entries[2].PropertyId == propertyId); - if (_entries[3].PropertyId >= propertyId) - return (3, _entries[3].PropertyId == propertyId); - if (_entries[4].PropertyId >= propertyId) - return (4, _entries[4].PropertyId == propertyId); - if (_entries[5].PropertyId >= propertyId) - return (5, _entries[5].PropertyId == propertyId); - if (_entries[6].PropertyId >= propertyId) - return (6, _entries[6].PropertyId == propertyId); - if (_entries[7].PropertyId >= propertyId) - return (7, _entries[7].PropertyId == propertyId); - if (_entries[8].PropertyId >= propertyId) - return (8, _entries[8].PropertyId == propertyId); - if (_entries[9].PropertyId >= propertyId) - return (9, _entries[9].PropertyId == propertyId); - if (_entries[10].PropertyId >= propertyId) - return (10, _entries[10].PropertyId == propertyId); - } - else - { - int low = 0; - int high = _entries.Length; - int id; - - while (high - low > 3) - { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; - - if (propertyId == id) - return (pivot, true); - - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; - } - - do - { - id = _entries[low].PropertyId; - - if (id == propertyId) - return (low, true); - - if (id > propertyId) - break; - - ++low; - } - while (low < high); - } - - return (0, false); - } - - public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) - { - (int index, bool found) = TryFindEntry(property.Id); - if (!found) - { - value = default; - return false; - } - - value = _entries[index].Value; - return true; - } - - public void AddValue(AvaloniaProperty property, TValue value) - { - Entry[] entries = new Entry[_entries.Length + 1]; - - for (int i = 0; i < _entries.Length; ++i) - { - if (_entries[i].PropertyId > property.Id) - { - if (i > 0) - { - Array.Copy(_entries, 0, entries, 0, i); - } - - entries[i] = new Entry { PropertyId = property.Id, Value = value }; - Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); - break; - } - } - - _entries = entries; - } - - public void SetValue(AvaloniaProperty property, TValue value) - { - _entries[TryFindEntry(property.Id).Item1].Value = value; - } - - public void Remove(AvaloniaProperty property) - { - var (index, found) = TryFindEntry(property.Id); - - if (found) - { - var newLength = _entries.Length - 1; - - // Special case - one element left means that value store is empty so we can just reuse our "empty" array. - if (newLength == 1) - { - _entries = s_emptyEntries; - - return; - } - - var entries = new Entry[newLength]; - - int ix = 0; - - for (int i = 0; i < _entries.Length; ++i) - { - if (i != index) - { - entries[ix++] = _entries[i]; - } - } - - _entries = entries; - } - } - - private struct Entry - { - internal int PropertyId; - internal TValue Value; - } - } -} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs deleted file mode 100644 index bf29e0b0ac..0000000000 --- a/src/Avalonia.Base/ValueStore.cs +++ /dev/null @@ -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 -{ - /// - /// Stores styled property values for an . - /// - /// - /// At its core this class consists of an to - /// mapping which holds the current values for each set property. This - /// can be in one of 4 states: - /// - /// - For a single local value it will be an instance of . - /// - For a single value of a priority other than LocalValue it will be an instance of - /// ` - /// - For a single binding it will be an instance of - /// - For all other cases it will be an instance of - /// - internal class ValueStore - { - private readonly AvaloniaObject _owner; - private readonly AvaloniaPropertyValueStore _values; - private BatchUpdate? _batchUpdate; - - public ValueStore(AvaloniaObject owner) - { - _owner = owner; - _values = new AvaloniaPropertyValueStore(); - } - - 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( - StyledPropertyBase property, - BindingPriority maxPriority, - out T value) - { - if (TryGetValue(property, out var slot)) - { - var v = ((IValue)slot).GetValue(maxPriority); - - if (v.HasValue) - { - value = v.Value; - return true; - } - } - - value = default!; - return false; - } - - public IDisposable? SetValue(StyledPropertyBase 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(_owner, property, this); - AddValue(property, entry); - result = entry.SetValue(value, priority); - } - else - { - if (priority == BindingPriority.LocalValue) - { - AddValue(property, new LocalValueEntry(value)); - NotifyValueChanged(property, default, value, priority); - } - else - { - var entry = new ConstantValueEntry(property, value, priority, new(this)); - AddValue(property, entry); - NotifyValueChanged(property, default, value, priority); - result = entry; - } - } - - return result; - } - - public IDisposable AddBinding( - StyledPropertyBase property, - IObservable> 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(_owner, property, this); - var binding = entry.AddBinding(source, priority); - AddValue(property, entry); - return binding; - } - else - { - var entry = new BindingEntry(_owner, property, source, priority, new(this)); - AddValue(property, entry); - return entry; - } - } - - public void ClearLocalValue(StyledPropertyBase property) - { - if (TryGetValue(property, out var slot)) - { - if (slot is PriorityValue p) - { - p.ClearLocalValue(); - } - else if (slot.Priority == BindingPriority.LocalValue) - { - var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? - new Optional(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(property, Optional.Empty, BindingPriority.Unset, new(this)); - _values.SetValue(property, sentinel); - } - - NotifyValueChanged(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(AvaloniaPropertyChangedEventArgs change) - { - if (_batchUpdate is object) - { - if (change.IsEffectiveValueChange) - { - NotifyValueChanged(change.Property, change.OldValue, change.NewValue, change.Priority); - } - } - else - { - _owner.ValueChanged(change); - } - } - - public void Completed( - StyledPropertyBase property, - IPriorityValueEntry entry, - Optional 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( - object slot, - StyledPropertyBase property, - T value, - BindingPriority priority) - { - IDisposable? result = null; - - if (slot is IPriorityValueEntry e) - { - var priorityValue = new PriorityValue(_owner, property, this, e); - _values.SetValue(property, priorityValue); - result = priorityValue.SetValue(value, priority); - } - else if (slot is PriorityValue p) - { - result = p.SetValue(value, priority); - } - else if (slot is LocalValueEntry l) - { - if (priority == BindingPriority.LocalValue) - { - var old = l.GetValue(BindingPriority.LocalValue); - l.SetValue(value); - NotifyValueChanged(property, old, value, priority); - } - else - { - var priorityValue = new PriorityValue(_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( - object slot, - StyledPropertyBase property, - IObservable> source, - BindingPriority priority) - { - PriorityValue priorityValue; - - if (slot is IPriorityValueEntry e) - { - priorityValue = new PriorityValue(_owner, property, this, e); - - if (IsBatchUpdating()) - { - priorityValue.BeginBatchUpdate(); - } - } - else if (slot is PriorityValue p) - { - priorityValue = p; - } - else if (slot is LocalValueEntry l) - { - priorityValue = new PriorityValue(_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( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) - { - if (_batchUpdate is null) - { - _owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( - _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? _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. - 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 oldValue) - { - _notifications ??= new List(); - - 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 oldValue; - } - } - } -} diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 69389def56..29559a8618 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -536,27 +536,24 @@ namespace Avalonia BindingPriority.LocalValue); } - protected internal sealed override void LogBindingError(AvaloniaProperty property, Exception e) + internal override ParametrizedLogger? GetBindingWarningLogger( + AvaloniaProperty property, + Exception? e) { - // Don't log a binding error unless the control is attached to a logical tree. - if (((ILogical)this).IsAttachedToLogicalTree) - { - if (e is BindingChainException b && - string.IsNullOrEmpty(b.ExpressionErrorPoint) && - DataContext == null) - { - // The error occurred at the root of the binding chain and DataContext is null; - // don't log this - the DataContext probably hasn't been set up yet. - return; - } + // Don't log a binding error unless the control is attached to the logical tree. + if (!((ILogical)this).IsAttachedToLogicalTree) + return null; - Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( - this, - "Error in binding to {Target}.{Property}: {Message}", - this, - property, - e.Message); + if (e is BindingChainException b && + string.IsNullOrEmpty(b.ExpressionErrorPoint) && + DataContext == null) + { + // The error occurred at the root of the binding chain and DataContext is null; + // don't log this - the DataContext probably hasn't been set up yet. + return null; } + + return Logger.TryGet(LogEventLevel.Warning, LogArea.Binding); } /// diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 2441da8920..80151fbfb3 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -6,6 +6,7 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Styling; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -364,17 +365,6 @@ namespace Avalonia.Controls.Primitives { } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == ThemeProperty) - { - foreach (var child in this.GetTemplateChildren()) - child.InvalidateStyles(); - } - } - /// /// Called when the property changes. /// @@ -404,5 +394,57 @@ namespace Avalonia.Controls.Primitives } } } + + internal override void DetachControlThemeFromTemplateChildren(ControlTheme theme) + { + static ControlTheme? GetControlTheme(StyleBase style) + { + var s = style; + + while (s is not null) + { + if (s is ControlTheme c) + return c; + s = s.Parent as StyleBase; + } + + return null; + } + + static void Detach(Visual control, ITemplatedControl templatedParent, ControlTheme theme) + { + var valueStore = control.GetValueStore(); + var count = valueStore.Frames.Count; + + if (control != templatedParent) + { + valueStore.BeginStyling(); + + for (var i = count - 1; i >= 0; --i) + { + if (valueStore.Frames[i] is StyleInstance si && + si.Source is StyleBase style && + GetControlTheme(style) == theme) + { + valueStore.RemoveFrame(si); + } + } + + valueStore.EndStyling(); + } + + var children = ((IVisual)control).VisualChildren; + count = children.Count; + + for (var i = 0; i < count; i++) + { + if (children[i] is Visual v && + v.TemplatedParent == templatedParent) + Detach(v, templatedParent, theme); + } + } + + Detach(this, this, theme); + } } } diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index d95e7dd1ef..3ce4730698 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -70,7 +70,6 @@ - diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs index f9ff8caab1..19ae0dc260 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs @@ -413,25 +413,86 @@ namespace Avalonia.Base.UnitTests.Animation } [Fact] - public void Transitions_Can_Re_Set_During_Batch_Update() + public void Transitions_Can_Re_Set_During_Styling() { var target = CreateTarget(); var control = CreateControl(target.Object); // Assigning and then clearing Transitions ensures we have a transition state // collection created. - control.Transitions = null; + control.ClearValue(Control.TransitionsProperty); - control.BeginBatchUpdate(); + control.GetValueStore().BeginStyling(); // Setting opacity then Transitions means that we receive the Transitions change - // after the Opacity change when EndBatchUpdate is called. - control.Opacity = 0.5; - control.Transitions = new Transitions { target.Object }; + // after the Opacity change when EndStyling is called. + var style = new Style + { + Setters = + { + new Setter(Control.OpacityProperty, 0.5), + new Setter(Control.TransitionsProperty, new Transitions { target.Object }), + } + }; + + style.TryAttach(control, control); // Which means that the transition state hasn't been initialized with the new // Transitions when the Opacity change notification gets raised here. - control.EndBatchUpdate(); + control.GetValueStore().EndStyling(); + } + + [Fact] + public void Transitions_Can_Be_Removed_While_Transition_In_Progress() + { + using var app = Start(); + + var opacityTransition = new DoubleTransition + { + Property = Control.OpacityProperty, + Duration = TimeSpan.FromSeconds(1), + }; + + var transitions = new Transitions { opacityTransition }; + var borderTheme = new ControlTheme(typeof(Border)) + { + Setters = + { + new Setter(Control.TransitionsProperty, transitions), + } + }; + + var clock = new TestClock(); + var root = new TestRoot + { + Clock = clock, + Resources = + { + { typeof(Border), borderTheme }, + } + }; + + var border = new Border(); + root.Child = border; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + Assert.Same(transitions, border.Transitions); + + // First set property with a transition to a new value, and step the clock until + // transition is complete. + border.Opacity = 0; + clock.Step(TimeSpan.FromSeconds(0)); + clock.Step(TimeSpan.FromSeconds(1)); + Assert.Equal(0, border.Opacity); + + // Now clear the property; a transition is now in progress but no local value is + // set. + border.ClearValue(Border.OpacityProperty); + + // Remove the transition by removing the control from the logical tree. This was + // causing an exception. + root.Child = null; } private static IDisposable Start() diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs deleted file mode 100644 index 45de860894..0000000000 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ /dev/null @@ -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(); - - 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("foo"); - var raised = new List(); - - 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("foo"); - var raised = new List(); - - 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("foo"); - var raised = new List(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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("baz"); - var raised = new List(); - - 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("foo"); - var raised = new List(); - - 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("foo"); - var observable2 = new TestObservable("qux"); - var raised = new List(); - - 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("foo"); - var raised = new List(); - - 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("bar"); - var observable2 = new TestObservable("baz"); - var raised = new List(); - - 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.Horizontal); - var raised = new List(); - - 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("foo"); - var raised = new List(); - - 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("foo"); - var raised = new List(); - - 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(); - - 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("foo"); - var observable2 = new TestObservable("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("foo"); - var observable2 = new TestObservable("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("foo"); - var observable2 = new TestObservable("bar"); - var raised = new List(); - - 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("foo"); - var observable2 = new TestObservable("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("foo"); - var observable2 = new TestObservable("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("foo"); - var observable2 = new TestObservable("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(); - - 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(); - - 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(); - - 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(); - - 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("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(); - var observable1 = new TestObservable("foo"); - var observable2 = new TestObservable("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(); - - 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 FooProperty = - AvaloniaProperty.Register(nameof(Foo)); - - public static readonly StyledProperty BarProperty = - AvaloniaProperty.Register(nameof(Bar)); - - public static readonly StyledProperty BazProperty = - AvaloniaProperty.Register(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 : ObservableBase> - { - private readonly T _value; - private IObserver> _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> observer) - { - ++SubscribeCount; - _observer = observer; - observer.OnNext(_value); - return Disposable.Empty; - } - } - } -} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 6b4a6f89df..6db339c4cd 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -4,18 +4,18 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading; using System.Threading.Tasks; - +using Avalonia.Base.UnitTests.Styling; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; -using Avalonia.Utilities; -using Microsoft.Reactive.Testing; using Moq; using Xunit; +#nullable enable + namespace Avalonia.Base.UnitTests { public class AvaloniaObjectTests_Binding @@ -24,11 +24,10 @@ namespace Avalonia.Base.UnitTests public void Bind_Sets_Current_Value() { var target = new Class1(); - var source = new Class1(); + var source = new BehaviorSubject>("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>(); - 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(); + var source = new Subject>(); 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(); + var source = new Subject>(); 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("bar"); + var source = new BehaviorSubject>("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>("foo"); + var source2 = new BehaviorSubject>("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>("foo"); + var source2 = new BehaviorSubject>("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(); + var source = new Subject>(); 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("bar"), BindingPriority.Style); + target.Bind(property, new BehaviorSubject>("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(); - var source2 = new Subject(); + var source1 = new Subject>(); + var source2 = new Subject>(); 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(); - var source2 = new Subject(); + var source1 = new Subject>(); + var source2 = new Subject>(); 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(); - var source2 = new Subject(); + var source1 = new Subject>(); + var source2 = new Subject>(); 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(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(); @@ -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(); + + 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(); + + 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(); + + 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().StartWith("foo")); + target.Bind(Class2.BarProperty, Observable.Never>().StartWith("foo")); Assert.Equal("foo", target.GetValue(Class2.BarProperty)); } @@ -403,17 +489,122 @@ namespace Avalonia.Base.UnitTests [Fact] public void Observable_Is_Unsubscribed_When_Subscription_Disposed() { - var scheduler = new TestScheduler(); - var source = scheduler.CreateColdObservable(); + var source = new TestSubject>("foo"); var target = new Class1(); var subscription = target.Bind(Class1.FooProperty, source); - Assert.Equal(1, source.Subscriptions.Count); - Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + Assert.Equal(1, source.SubscriberCount); subscription.Dispose(); - Assert.Equal(1, source.Subscriptions.Count); - Assert.Equal(0, source.Subscriptions[0].Unsubscribe); + Assert.Equal(0, source.SubscriberCount); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void Observable_Is_Unsubscribed_When_New_Binding_Of_Same_Priority_Is_Added(BindingPriority priority) + { + var source1 = new TestSubject>("foo"); + var source2 = new TestSubject>("bar"); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source1, priority); + Assert.Equal(1, source1.SubscriberCount); + + target.Bind(Class1.FooProperty, source2, priority); + Assert.Equal(1, source2.SubscriberCount); + Assert.Equal(0, source1.SubscriberCount); + } + + [Theory] + [InlineData(BindingPriority.Style)] + public void Observable_Is_Unsubscribed_When_New_Binding_Of_Higher_Priority_Is_Added(BindingPriority priority) + { + var source1 = new TestSubject>("foo"); + var source2 = new TestSubject>("bar"); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source1, priority); + Assert.Equal(1, source1.SubscriberCount); + + target.Bind(Class1.FooProperty, source2, priority - 1); + Assert.Equal(1, source2.SubscriberCount); + Assert.Equal(0, source1.SubscriberCount); + } + + [Theory] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void Observable_Is_Unsubscribed_When_New_Value_Of_Same_Priority_Is_Added(BindingPriority priority) + { + var source = new TestSubject>("foo"); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source, priority); + Assert.Equal(1, source.SubscriberCount); + + target.SetValue(Class1.FooProperty, "foo", priority); + Assert.Equal(0, source.SubscriberCount); + } + + [Theory] + [InlineData(BindingPriority.Style)] + public void Observable_Is_Unsubscribed_When_New_Value_Of_Higher_Priority_Is_Added(BindingPriority priority) + { + var source = new TestSubject>("foo"); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source, priority); + Assert.Equal(1, source.SubscriberCount); + + target.SetValue(Class1.FooProperty, "foo", priority - 1); + Assert.Equal(0, source.SubscriberCount); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Observable_Is_Not_Unsubscribed_When_Animation_Value_Is_Set(BindingPriority priority) + { + var source = new TestSubject>("foo"); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source, priority); + Assert.Equal(1, source.SubscriberCount); + + target.SetValue(Class1.FooProperty, "bar", BindingPriority.Animation); + Assert.Equal(1, source.SubscriberCount); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Observable_Is_Not_Unsubscribed_When_Animation_Binding_Is_Added(BindingPriority priority) + { + var source1 = new TestSubject>("foo"); + var source2 = new TestSubject>("bar"); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source1, priority); + Assert.Equal(1, source1.SubscriberCount); + + target.Bind(Class1.FooProperty, source2, BindingPriority.Animation); + Assert.Equal(1, source1.SubscriberCount); + Assert.Equal(1, source2.SubscriberCount); + } + + [Fact] + public void LocalValue_Binding_Is_Not_Unsubscribed_When_LocalValue_Is_Set() + { + var source = new TestSubject>("foo"); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source); + Assert.Equal(1, source.SubscriberCount); + + target.SetValue(Class1.FooProperty, "foo"); + Assert.Equal(1, source.SubscriberCount); } [Fact] @@ -482,7 +673,7 @@ namespace Avalonia.Base.UnitTests public void Local_Binding_Overwrites_Local_Value() { var target = new Class1(); - var binding = new Subject(); + var binding = new Subject>(); target.Bind(Class1.FooProperty, binding); @@ -660,6 +851,75 @@ namespace Avalonia.Base.UnitTests } } + [Fact] + public void Untyped_LocalValue_Binding_Logs_Invalid_Value_Type() + { + var target = new Class1(); + var source = new Subject(); + 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(); + var called = false; + var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}"; + var expectedMessage = "Unable to convert object 'foo' of type 'System.String' to type 'System.Double'."; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level == LogEventLevel.Warning && + area == LogArea.Binding && + mt == expectedMessageTemplate && + src == target && + pv[0].GetType() == typeof(Class1) && + (AvaloniaProperty)pv[1] == Class1.QuxProperty && + (string)pv[2] == expectedMessage) + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Class1.QuxProperty, source, BindingPriority.Style); + source.OnNext(1.2); + source.OnNext("foo"); + + Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + Assert.True(called); + } + } + [Fact] public async Task Bind_With_Scheduler_Executes_On_Scheduler() { @@ -726,8 +986,9 @@ namespace Avalonia.Base.UnitTests public void IsAnimating_On_Property_With_Animation_Value_Returns_True() { var target = new Class1(); + var source = new BehaviorSubject>("foo"); - target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation); + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); Assert.True(target.IsAnimating(Class1.FooProperty)); } @@ -778,6 +1039,20 @@ namespace Avalonia.Base.UnitTests Assert.True(target.IsAnimating(Class1.FooProperty)); } + [Fact] + public void TwoWay_Binding_Should_Update_Source() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + + target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); + + target.DoubleValue = 123.4; + + Assert.True(source.SetterCalled); + Assert.Equal(source.Value, 123.4); + } + [Fact] public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation() { @@ -786,7 +1061,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); - Assert.False(source.ValueSetterCalled); + Assert.False(source.SetterCalled); } [Fact] @@ -797,7 +1072,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source }); - Assert.False(source.ValueSetterCalled); + Assert.False(source.SetterCalled); } [Fact] @@ -818,69 +1093,111 @@ namespace Avalonia.Base.UnitTests target.Bind(TextBlock.TextProperty, new Binding("[0]", BindingMode.TwoWay)); } - [Fact] - public void Disposing_Completed_Binding_Does_Not_Throw() + [Theory(Skip = "Will need changes to binding internals in order to pass")] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.StyleTrigger)] + [InlineData(BindingPriority.Style)] + public void TwoWay_Binding_Should_Not_Update_Source_When_Higher_Priority_Value_Set(BindingPriority priority) { var target = new Class1(); - var source = new Subject(); - var subscription = target.Bind(Class1.FooProperty, source); + var source = new TestTwoWayBindingViewModel(); + var binding = new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }; - source.OnCompleted(); + target.Bind(Class1.DoubleValueProperty, binding, priority); + target.SetValue(Class1.DoubleValueProperty, 123.4, priority - 1); - subscription.Dispose(); + // Setter should not be called because the TwoWay binding with LocalValue priority + // should be overridden by the animated value and the binding made inactive. + Assert.False(source.SetterCalled); } - [Fact] - public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_With_Value() + [Theory(Skip = "Will need changes to binding internals in order to pass")] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.StyleTrigger)] + [InlineData(BindingPriority.Style)] + public void TwoWay_Binding_Should_Not_Update_Source_When_Higher_Priority_Binding_Added(BindingPriority priority) + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + var binding1 = new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }; + var binding2 = new BehaviorSubject(123.4); + + target.Bind(Class1.DoubleValueProperty, binding1, priority); + target.Bind(Class1.DoubleValueProperty, binding2, priority - 1); + + // Setter should not be called because the TwoWay binding with LocalValue priority + // should be overridden by the animated binding and the binding made inactive. + Assert.False(source.SetterCalled); + } + + [Fact(Skip = "Will need changes to binding internals in order to pass")] + public void TwoWay_Style_Binding_Should_Not_Update_Source_When_StyleTrigger_Value_Set() { var target = new Class1(); - var source = new TestTwoWayBindingViewModel() { Value = 1 }; - source.ResetSetterCalled(); + var source = new TestTwoWayBindingViewModel(); target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); + target.SetValue(Class1.DoubleValueProperty, 123.4, BindingPriority.Animation); - Assert.False(source.ValueSetterCalled); + // Setter should not be called because the TwoWay binding with Style priority + // should be overridden by the animated value and the binding made inactive. + Assert.False(source.SetterCalled); } - [Fact] - public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer_With_Value() + [Fact(Skip = "Will need changes to binding internals in order to pass")] + public void TwoWay_Style_Binding_Should_Not_Update_Source_When_Animated_Binding_Added() { var target = new Class1(); - var source = new TestTwoWayBindingViewModel() { [0] = 1 }; - source.ResetSetterCalled(); + var source1 = new TestTwoWayBindingViewModel(); + var source2 = new BehaviorSubject(123.4); - target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source }); + target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source1.Value), BindingMode.TwoWay) { Source = source1 }); + target.Bind(Class1.DoubleValueProperty, source2, BindingPriority.Animation); - Assert.False(source.ValueSetterCalled); + // Setter should not be called because the TwoWay binding with Style priority + // should be overridden by the animated binding and the binding made inactive. + Assert.False(source1.SetterCalled); } + [Fact] + public void Disposing_Completed_Binding_Does_Not_Throw() + { + var target = new Class1(); + var source = new Subject>(); + var subscription = target.Bind(Class1.FooProperty, source); + + source.OnCompleted(); + + subscription.Dispose(); + } [Fact] - public void Disposing_a_TwoWay_Binding_Should_Set_Default_Value_On_Binding_Target_But_Not_On_Source() + public void Produces_Correct_Values_And_Base_Values_With_Multiple_Animation_Bindings() { - var target = new Class3(); + var target = new Class1(); + var source1 = new BehaviorSubject>(12.2); + var source2 = new BehaviorSubject>(13.3); - // Create a source class which has a Value set to -1 and a Minimum set to -2 - var source = new TestTwoWayBindingViewModel() { Value = -1, Minimum = -2 }; + target.SetValue(Class1.QuxProperty, 11.1); + target.Bind(Class1.QuxProperty, source1, BindingPriority.Animation); - // Reset the setter counter - source.ResetSetterCalled(); + Assert.Equal(12.2, target.GetValue(Class1.QuxProperty)); + Assert.Equal(11.1, target.GetBaseValue(Class1.QuxProperty)); - // 1. bind the minimum - var disposable_1 = target.Bind(Class3.MinimumProperty, new Binding("Minimum", BindingMode.TwoWay) { Source = source }); - // 2. Bind the value - var disposable_2 = target.Bind(Class3.ValueProperty, new Binding("Value", BindingMode.TwoWay) { Source = source }); + target.Bind(Class1.QuxProperty, source2, BindingPriority.Animation); - // Dispose the minimum binding - disposable_1.Dispose(); - // Dispose the value binding - disposable_2.Dispose(); + Assert.Equal(13.3, target.GetValue(Class1.QuxProperty)); + Assert.Equal(11.1, target.GetBaseValue(Class1.QuxProperty)); + source2.OnCompleted(); - // The value setter should be called here as we have disposed minimum fist and the default value of minimum is 0, so this should be changed. - Assert.True(source.ValueSetterCalled); - // The minimum value should not be changed in the source. - Assert.False(source.MinimumSetterCalled); + Assert.Equal(12.2, target.GetValue(Class1.QuxProperty)); + Assert.Equal(11.1, target.GetBaseValue(Class1.QuxProperty)); + + source1.OnCompleted(); + + Assert.Equal(11.1, target.GetValue(Class1.QuxProperty)); + Assert.Equal(11.1, target.GetBaseValue(Class1.QuxProperty)); } /// @@ -889,9 +1206,9 @@ namespace Avalonia.Base.UnitTests /// The type of the observable. /// The value. /// The observable. - private IObservable Single(T value) + private IObservable> Single(T value) { - return Observable.Never().StartWith(value); + return Observable.Never>().StartWith(value); } private class Class1 : AvaloniaObject @@ -918,56 +1235,6 @@ namespace Avalonia.Base.UnitTests AvaloniaProperty.Register("Bar", "bardefault"); } - private class Class3 : AvaloniaObject - { - static Class3() - { - MinimumProperty.Changed.Subscribe(x => OnMinimumChanged(x)); - } - - private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is Class3 s) - { - s.SetValue(ValueProperty, MathUtilities.Clamp(s.Value, e.NewValue.Value, double.PositiveInfinity)); - } - } - - /// - /// Defines the property. - /// - public static readonly StyledProperty ValueProperty = - AvaloniaProperty.Register(nameof(Value), 0); - - /// - /// Gets or sets the Value property - /// - public double Value - { - get { return GetValue(ValueProperty); } - set { SetValue(ValueProperty, value); } - } - - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register(nameof(Minimum), 0); - - /// - /// Gets or sets the minimum property - /// - public double Minimum - { - get { return GetValue(MinimumProperty); } - set { SetValue(MinimumProperty, value); } - } - - - } - - private class TestOneTimeBinding : IBinding { private IObservable _source; @@ -979,8 +1246,8 @@ namespace Avalonia.Base.UnitTests public InstancedBinding Initiate( IAvaloniaObject target, - AvaloniaProperty targetProperty, - object anchor = null, + AvaloniaProperty? targetProperty, + object? anchor = null, bool enableDataValidation = false) { return InstancedBinding.OneTime(_source); @@ -995,7 +1262,7 @@ namespace Avalonia.Base.UnitTests private double _value; - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; public double Value { @@ -1008,8 +1275,10 @@ namespace Avalonia.Base.UnitTests if (SetterInvokedCount < MaxInvokedCount) { _value = (int)value; - if (_value > 75) _value = 75; - if (_value < 25) _value = 25; + if (_value > 75) + _value = 75; + if (_value < 25) + _value = 25; } else { @@ -1032,18 +1301,7 @@ namespace Avalonia.Base.UnitTests set { _value = value; - ValueSetterCalled = true; - } - } - - private double _minimum; - public double Minimum - { - get => _minimum; - set - { - _minimum = value; - MinimumSetterCalled = true; + SetterCalled = true; } } @@ -1053,18 +1311,11 @@ namespace Avalonia.Base.UnitTests set { _value = value; - ValueSetterCalled = true; + SetterCalled = true; } } - public bool ValueSetterCalled { get; private set; } - public bool MinimumSetterCalled { get; private set; } - - public void ResetSetterCalled() - { - ValueSetterCalled = false; - MinimumSetterCalled = false; - } + public bool SetterCalled { get; private set; } } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index e2e87d4dfa..2e1944fc76 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reactive.Subjects; using Avalonia.Data; using Xunit; @@ -52,6 +53,64 @@ namespace Avalonia.Base.UnitTests Assert.Equal(50, target.Foo); } + [Fact] + public void CoerceValue_Updates_Base_Value() + { + var target = new Class1 { Foo = 99 }; + + target.SetValue(Class1.FooProperty, 88, BindingPriority.Animation); + + Assert.Equal(88, target.Foo); + Assert.Equal(99, target.GetBaseValue(Class1.FooProperty)); + + target.MaxFoo = 50; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(50, target.Foo); + Assert.Equal(50, target.GetBaseValue(Class1.FooProperty)); + } + + [Fact] + public void CoerceValue_Raises_PropertyChanged() + { + var target = new Class1 { Foo = 99 }; + var raised = 0; + + target.PropertyChanged += (s, e) => + { + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal(99, e.OldValue); + Assert.Equal(50, e.NewValue); + Assert.Equal(BindingPriority.LocalValue, e.Priority); + ++raised; + }; + + Assert.Equal(99, target.Foo); + + target.MaxFoo = 50; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(50, target.Foo); + Assert.Equal(1, raised); + } + + [Fact] + public void CoerceValue_Raises_PropertyChangedCore_For_Base_Value() + { + var target = new Class1 { Foo = 99 }; + + target.SetValue(Class1.FooProperty, 88, BindingPriority.Animation); + + Assert.Equal(88, target.Foo); + Assert.Equal(99, target.GetBaseValue(Class1.FooProperty)); + + target.MaxFoo = 50; + target.CoreChanges.Clear(); + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(2, target.CoreChanges.Count); + } + [Fact] public void Coerced_Value_Can_Be_Restored_If_Limit_Changed() { @@ -73,7 +132,7 @@ namespace Avalonia.Base.UnitTests var source1 = new Subject>(); var source2 = new Subject>(); - target.Bind(Class1.FooProperty, source1); + target.Bind(Class1.FooProperty, source1, BindingPriority.Style); source1.OnNext(150); target.Bind(Class1.FooProperty, source2); @@ -87,6 +146,32 @@ namespace Avalonia.Base.UnitTests Assert.Equal(150, target.Foo); } + [Fact] + public void CoerceValue_Updates_Inherited_Value() + { + var parent = new Class1 { Inherited = 99 }; + var child = new AvaloniaObject { InheritanceParent = parent }; + var raised = 0; + + child.InheritanceParent = parent; + child.PropertyChanged += (s, e) => + { + Assert.Equal(Class1.InheritedProperty, e.Property); + Assert.Equal(99, e.OldValue); + Assert.Equal(50, e.NewValue); + Assert.Equal(BindingPriority.Inherited, e.Priority); + ++raised; + }; + + Assert.Equal(99, child.GetValue(Class1.InheritedProperty)); + + parent.MaxFoo = 50; + parent.CoerceValue(Class1.InheritedProperty); + + Assert.Equal(50, child.GetValue(Class1.InheritedProperty)); + Assert.Equal(1, raised); + } + [Fact] public void Coercion_Can_Be_Overridden() { @@ -111,18 +196,51 @@ namespace Avalonia.Base.UnitTests defaultValue: 11, coerce: CoerceFoo); + public static readonly StyledProperty InheritedProperty = + AvaloniaProperty.RegisterAttached( + "Attached", + defaultValue: 11, + inherits: true, + coerce: CoerceFoo); + public int Foo { get => GetValue(FooProperty); set => SetValue(FooProperty, value); } + public int Inherited + { + get => GetValue(InheritedProperty); + set => SetValue(InheritedProperty, value); + } + public int MaxFoo { get; set; } = 100; + public List CoreChanges { get; } = new(); + public static int CoerceFoo(IAvaloniaObject instance, int value) { return Math.Min(((Class1)instance).MaxFoo, value); } + + protected override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + { + CoreChanges.Add(Clone(change)); + base.OnPropertyChangedCore(change); + } + + private static AvaloniaPropertyChangedEventArgs Clone(AvaloniaPropertyChangedEventArgs change) + { + var e = (AvaloniaPropertyChangedEventArgs)change; + return new AvaloniaPropertyChangedEventArgs( + change.Sender, + e.Property, + e.OldValue, + e.NewValue, + change.Priority, + change.IsEffectiveValueChange); + } } private class Class2 : AvaloniaObject diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs index 6bd29a1577..c20b75443c 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs +++ b/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("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("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 diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs index 9e9ae4ec74..c5cbda5325 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Data; using Xunit; namespace Avalonia.Base.UnitTests @@ -6,7 +7,28 @@ namespace Avalonia.Base.UnitTests public class AvaloniaObjectTests_Inheritance { [Fact] - public void GetValue_Returns_Inherited_Value() + public void GetValue_Returns_Inherited_Value_1() + { + Class1 parent = new Class1(); + parent.SetValue(Class1.BazProperty, "changed"); + + Class2 child = new Class2 { Parent = parent }; + Assert.Equal("changed", child.GetValue(Class1.BazProperty)); + } + + [Fact] + public void GetValue_Returns_Inherited_Value_2() + { + Class1 parent = new Class1(); + Class2 child = new Class2 { Parent = parent }; + + parent.SetValue(Class1.BazProperty, "changed"); + + Assert.Equal("changed", child.GetValue(Class1.BazProperty)); + } + + [Fact] + public void ClearValue_Clears_Inherited_Value() { Class1 parent = new Class1(); Class2 child = new Class2 { Parent = parent }; @@ -14,10 +36,62 @@ namespace Avalonia.Base.UnitTests parent.SetValue(Class1.BazProperty, "changed"); Assert.Equal("changed", child.GetValue(Class1.BazProperty)); + + parent.ClearValue(Class1.BazProperty); + + Assert.Equal("bazdefault", parent.GetValue(Class1.BazProperty)); + Assert.Equal("bazdefault", child.GetValue(Class1.BazProperty)); + } + + [Fact] + public void ClearValue_On_Parent_Raises_PropertyChanged_On_Child() + { + Class1 parent = new Class1(); + Class2 child = new Class2 { Parent = parent }; + var raised = 0; + + parent.SetValue(Class1.BazProperty, "changed"); + + child.PropertyChanged += (s, e) => + { + Assert.Same(child, e.Sender); + Assert.Equal("changed", e.OldValue); + Assert.Equal("bazdefault", e.NewValue); + Assert.Equal(BindingPriority.Inherited, e.Priority); + ++raised; + }; + + parent.ClearValue(Class1.BazProperty); + + Assert.Equal(1, raised); + } + + [Fact] + public void ClearValue_On_Child_Raises_PropertyChanged_With_Inherited_Parent_Value() + { + Class1 parent = new Class1(); + Class2 child = new Class2 { Parent = parent }; + var raised = 0; + + parent.SetValue(Class1.BazProperty, "parent"); + child.SetValue(Class1.BazProperty, "child"); + + child.PropertyChanged += (s, e) => + { + Assert.Same(child, e.Sender); + Assert.Equal("child", e.OldValue); + Assert.Equal("parent", e.NewValue); + Assert.Equal(BindingPriority.Inherited, e.Priority); + ++raised; + }; + + child.ClearValue(Class1.BazProperty); + + Assert.Equal(1, raised); } [Fact] - public void Setting_InheritanceParent_Raises_PropertyChanged_When_Value_Changed_In_Parent() + public void Setting_InheritanceParent_Raises_PropertyChanged_When_Parent_Has_Value_Set() { bool raised = false; @@ -29,15 +103,41 @@ namespace Avalonia.Base.UnitTests raised = s == child && e.Property == Class1.BazProperty && (string)e.OldValue == "bazdefault" && - (string)e.NewValue == "changed"; + (string)e.NewValue == "changed" && + e.Priority == BindingPriority.Inherited; + + child.Parent = parent; + + Assert.True(raised); + Assert.Equal("changed", child.GetValue(Class1.BazProperty)); + } + + [Fact] + public void Setting_InheritanceParent_Raises_PropertyChanged_When_Parent_And_Grandparent_Has_Value_Set() + { + Class1 grandparent = new Class1(); + Class2 parent = new Class2 { Parent = grandparent }; + bool raised = false; + + grandparent.SetValue(Class1.BazProperty, "changed1"); + parent.SetValue(Class1.BazProperty, "changed2"); + + Class2 child = new Class2(); + child.PropertyChanged += (s, e) => + raised = s == child && + e.Property == Class1.BazProperty && + (string)e.OldValue == "bazdefault" && + (string)e.NewValue == "changed2" && + e.Priority == BindingPriority.Inherited; child.Parent = parent; Assert.True(raised); + Assert.Equal("changed2", child.GetValue(Class1.BazProperty)); } [Fact] - public void Setting_InheritanceParent_Raises_PropertyChanged_For_Attached_Property_When_Value_Changed_In_Parent() + public void Setting_InheritanceParent_Raises_PropertyChanged_For_Attached_Property_When_Parent_Has_Value_Set() { bool raised = false; @@ -54,6 +154,7 @@ namespace Avalonia.Base.UnitTests child.Parent = parent; Assert.True(raised); + Assert.Equal("changed", child.GetValue(AttachedOwner.AttachedProperty)); } [Fact] @@ -71,6 +172,7 @@ namespace Avalonia.Base.UnitTests child.Parent = parent; Assert.False(raised); + Assert.Equal("localvalue", child.GetValue(Class1.BazProperty)); } [Fact] @@ -91,6 +193,7 @@ namespace Avalonia.Base.UnitTests parent.SetValue(Class1.BazProperty, "changed"); Assert.True(raised); + Assert.Equal("changed", child.GetValue(Class1.BazProperty)); } [Fact] @@ -111,6 +214,29 @@ namespace Avalonia.Base.UnitTests parent.SetValue(AttachedOwner.AttachedProperty, "changed"); Assert.True(raised); + Assert.Equal("changed", child.GetValue(AttachedOwner.AttachedProperty)); + } + + [Fact] + public void Clearing_Value_In_InheritanceParent_Raises_PropertyChanged() + { + bool raised = false; + + Class1 parent = new Class1(); + parent.SetValue(Class1.BazProperty, "changed"); + + Class2 child = new Class2 { Parent = parent }; + + child.PropertyChanged += (s, e) => + raised = s == child && + e.Property == Class1.BazProperty && + (string)e.OldValue == "changed" && + (string)e.NewValue == "bazdefault"; + + parent.ClearValue(Class1.BazProperty); + + Assert.True(raised); + Assert.Equal("bazdefault", child.GetValue(Class1.BazProperty)); } [Fact] @@ -128,6 +254,85 @@ namespace Avalonia.Base.UnitTests Assert.Equal(new[] { parent, child }, result); } + [Fact] + public void Reparenting_Raises_PropertyChanged_For_Old_And_New_Inherited_Values() + { + var oldParent = new Class1(); + oldParent.SetValue(Class1.BazProperty, "oldvalue"); + + var newParent = new Class1(); + newParent.SetValue(Class1.BazProperty, "newvalue"); + + var child = new Class2 { Parent = oldParent }; + var raised = 0; + + child.PropertyChanged += (s, e) => + { + Assert.Equal(child, e.Sender); + Assert.Equal("oldvalue", e.GetOldValue()); + Assert.Equal("newvalue", e.GetNewValue()); + 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()); + Assert.Equal("newvalue", e.GetNewValue()); + 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 FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs index 7f4dcace71..326199b3c2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Animation); target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style); Assert.Equal(2, target.CoreChanges.Count); @@ -49,38 +49,23 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void OnPropertyChangedCore_Is_Called_On_All_Binding_Property_Changes() + public void OnPropertyChangedCore_Is_Called_On_Non_Effective_Property_Binding_Value_Change() { var target = new Class1(); - var style = new Subject>(); - var animation = new Subject>(); - var templatedParent = new Subject>(); - - 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>(); - - Assert.Equal( - new[] { true, true, true, false, false, true, false, false, true }, - changes.Select(x => x.IsEffectiveValueChange).ToList()); - Assert.Equal( - new[] { "style1", "tp1", "a1", "tp2", "$unset", "a2", "style2", "$unset", "foodefault" }, - changes.Select(x => x.NewValue.GetValueOrDefault("$unset")).ToList()); - Assert.Equal( - new[] { "foodefault", "style1", "tp1", "$unset", "$unset", "a1", "$unset", "$unset", "a2" }, - changes.Select(x => x.OldValue.GetValueOrDefault("$unset")).ToList()); + var source = new BehaviorSubject>("styled1"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Style); + target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Animation); + source.OnNext("styled2"); + + Assert.Equal(3, target.CoreChanges.Count); + + var change = (AvaloniaPropertyChangedEventArgs)target.CoreChanges[2]; + + Assert.Equal("styled2", change.NewValue.Value); + Assert.False(change.OldValue.HasValue); + Assert.Equal(BindingPriority.Style, change.Priority); + Assert.False(change.IsEffectiveValueChange); } [Fact] @@ -88,7 +73,7 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Animation); target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style); Assert.Equal(1, target.Changes.Count); @@ -124,19 +109,13 @@ namespace Avalonia.Base.UnitTests private static AvaloniaPropertyChangedEventArgs Clone(AvaloniaPropertyChangedEventArgs change) { var e = (AvaloniaPropertyChangedEventArgs)change; - var result = new AvaloniaPropertyChangedEventArgs( + return new AvaloniaPropertyChangedEventArgs( change.Sender, e.Property, e.OldValue, e.NewValue, - change.Priority); - - if (!change.IsEffectiveValueChange) - { - result.MarkNonEffectiveValue(); - } - - return result; + change.Priority, + change.IsEffectiveValueChange); } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index c14332e1fe..e8175cf477 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/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(); @@ -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(); + + 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(); + + 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() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index f3f39b465b..232b6ccf73 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Styling; using Avalonia.Utilities; using Xunit; @@ -149,7 +150,7 @@ namespace Avalonia.Base.UnitTests internal override IDisposable RouteBind( AvaloniaObject o, - IObservable> source, + IObservable source, BindingPriority priority) { throw new NotImplementedException(); @@ -165,12 +166,7 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } - internal override object RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) - { - throw new NotImplementedException(); - } - - internal override void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject oldParent) + internal override object RouteGetBaseValue(AvaloniaObject o) { throw new NotImplementedException(); } @@ -183,7 +179,7 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } - internal override ISetterInstance CreateSetterInstance(IStyleable target, object value) + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index a9c62a3c4a..f911048960 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -3,14 +3,13 @@ using System.Collections.Generic; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; -using Microsoft.Reactive.Testing; +using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Threading; using Avalonia.UnitTests; +using Microsoft.Reactive.Testing; using Xunit; -using System.Threading.Tasks; -using Avalonia.Markup.Parsers; -using Avalonia.Threading; namespace Avalonia.Base.UnitTests.Data.Core { @@ -636,7 +635,25 @@ namespace Avalonia.Base.UnitTests.Data.Core target.Subscribe(x => result.Add(x)); } - + + [Fact] + public void RootGetter_Is_Reevaluated_On_Subscribe() + { + var data = "foo"; + var target = new ExpressionObserver(() => data, new EmptyExpressionNode(), new Subject(), null); + var result = new List(); + var sub = target.Subscribe(x => result.Add(x)); + + Assert.Equal(new object[] { "foo" }, result); + + sub.Dispose(); + data = "bar"; + + target.Subscribe(x => result.Add(x)); + + Assert.Equal(new object[] { "foo", "bar" }, result); + } + public class MyViewModelBase { public object Name => "Name"; } public class MyViewModel : MyViewModelBase { public new string Name => "NewName"; } diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs deleted file mode 100644 index aa5993f3b2..0000000000 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ /dev/null @@ -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 TestProperty = new StyledProperty( - "Test", - typeof(PriorityValueTests), - new StyledPropertyMetadata()); - - [Fact] - public void Constructor_Should_Set_Value_Based_On_Initial_Entry() - { - var target = new PriorityValue( - Owner, - TestProperty, - ValueStore, - new ConstantValueEntry( - 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( - 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( - 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( - Owner, - TestProperty, - ValueStore); - - target.SetValue("1", BindingPriority.Style); - target.SetValue("2", BindingPriority.Animation); - - var result = target.Entries - .OfType>() - .Select(x => x.GetValue().Value) - .ToList(); - - Assert.Equal(new[] { "1", "2" }, result); - } - - [Fact] - public void Priority_Should_Be_Set() - { - var target = new PriorityValue( - 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(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>() - .Select(x => x.Source) - .OfType() - .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(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>() - .Select(x => x.Source) - .OfType() - .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(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>() - .Select(x => x.Source) - .OfType() - .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(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>() - .Select(x => x.Source) - .OfType() - .Select(x => x.Id) - .ToList(); - - Assert.Equal(new[] { "2", "3", "1" }, result); - } - - [Fact] - public void Competed_Binding_Should_Be_Removed() - { - var target = new PriorityValue(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>() - .Select(x => x.Source) - .OfType() - .Select(x => x.Id) - .ToList(); - - Assert.Equal(new[] { "2", "1" }, result); - } - - [Fact] - public void Value_Should_Come_From_Last_Entry() - { - var target = new PriorityValue(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(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(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(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(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(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> - { - private IObserver> _observer; - - public Source(string id) => Id = id; - public string Id { get; } - - public IDisposable Subscribe(IObserver> observer) - { - _observer = observer; - observer.OnNext(Id); - return Disposable.Empty; - } - - public void OnCompleted() => _observer.OnCompleted(); - } - } -} diff --git a/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs b/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs new file mode 100644 index 0000000000..bb726a1d63 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Reactive.Subjects; +using Avalonia.Data; +using Avalonia.PropertyStore; +using Avalonia.Styling; +using Microsoft.Reactive.Testing; +using Xunit; +using static Microsoft.Reactive.Testing.ReactiveTest; + +#nullable enable + +namespace Avalonia.Base.UnitTests.PropertyStore +{ + public class ValueStoreTests_Frames + { + [Fact] + public void Adding_Frame_Raises_PropertyChanged() + { + var target = new Class1(); + var subject = new BehaviorSubject("bar"); + var result = new List(); + 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("bar"); + var result = new List(); + var style = new Style + { + Setters = + { + new Setter(Class1.FooProperty, "foo"), + new Setter(Class1.BarProperty, subject.ToBinding()), + } + }; + var frame = InstanceStyle(style, target); + target.GetValueStore().AddFrame(frame); + + target.PropertyChanged += (s, e) => + { + result.Add(new(e.Property, e.OldValue, e.NewValue)); + }; + + target.GetValueStore().RemoveFrame(frame); + + Assert.Equal(new PropertyChange[] + { + new(Class1.BarProperty, "bar", "bardefault"), + new(Class1.FooProperty, "foo", "foodefault"), + }, result); + } + + [Fact] + public void Removing_Frame_Unsubscribes_Binding() + { + var target = new Class1(); + var scheduler = new TestScheduler(); + var obs = scheduler.CreateColdObservable(OnNext(0, "bar")); + var style = new Style + { + Setters = + { + new Setter(Class1.FooProperty, "foo"), + new Setter(Class1.BarProperty, obs.ToBinding()), + } + }; + var frame = InstanceStyle(style, target); + + target.GetValueStore().AddFrame(frame); + target.GetValueStore().RemoveFrame(frame); + + Assert.Single(obs.Subscriptions); + Assert.Equal(0, obs.Subscriptions[0].Subscribe); + Assert.NotEqual(Subscription.Infinite, obs.Subscriptions[0].Unsubscribe); + } + + [Fact] + public void Completing_Binding_Removes_ImmediateValueFrame() + { + var target = new Class1(); + var source = new BehaviorSubject>("foo"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + var valueStore = target.GetValueStore(); + Assert.Equal(1, valueStore.Frames.Count); + Assert.IsType(valueStore.Frames[0]); + + source.OnCompleted(); + + Assert.Equal(0, valueStore.Frames.Count); + } + + private static StyleInstance InstanceStyle(Style style, StyledElement target) + { + var result = new StyleInstance(style, null); + + foreach (var setter in style.Setters) + result.Add(setter.Instance(result, target)); + + return result; + } + + private class Class1 : StyledElement + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", "foodefault"); + + public static readonly StyledProperty BarProperty = + AvaloniaProperty.Register("Bar", "bardefault", true); + } + + private record PropertyChange( + AvaloniaProperty Property, + object? OldValue, + object? NewValue); + } +} diff --git a/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Inheritance.cs b/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Inheritance.cs new file mode 100644 index 0000000000..ed122e4ddc --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Inheritance.cs @@ -0,0 +1,134 @@ +using Xunit; + +#nullable enable + +namespace Avalonia.Base.UnitTests.PropertyStore +{ + public class ValueStoreTests_Inheritance + { + [Fact] + public void InheritanceAncestor_Is_Initially_Null() + { + var parent = new Class1(); + var child = new Class1 { Parent = parent }; + var grandchild = new Class1 { Parent = child }; + + Assert.Null(parent.GetValueStore().InheritanceAncestor); + Assert.Null(child.GetValueStore().InheritanceAncestor); + Assert.Null(grandchild.GetValueStore().InheritanceAncestor); + } + + [Fact] + public void Setting_Value_In_Parent_Updates_InheritanceAncestor() + { + var parent = new Class1(); + var child = new Class1 { Parent = parent }; + var grandchild = new Class1 { Parent = child }; + + parent.Foo = "changed"; + + var parentStore = parent.GetValueStore(); + Assert.Null(parentStore.InheritanceAncestor); + Assert.Same(parentStore, child.GetValueStore().InheritanceAncestor); + Assert.Same(parentStore, grandchild.GetValueStore().InheritanceAncestor); + } + + [Fact] + public void Setting_Value_In_Parent_Doesnt_Update_Grandchild_InheritanceAncestor_If_Child_Has_Value_Set() + { + var parent = new Class1(); + var child = new Class1 { Parent = parent }; + var grandchild = new Class1 { Parent = child }; + + child.Foo = "foochanged"; + parent.Foo = "changed"; + + var parentStore = parent.GetValueStore(); + Assert.Null(parentStore.InheritanceAncestor); + Assert.Same(parentStore, child.GetValueStore().InheritanceAncestor); + Assert.Same(child.GetValueStore(), grandchild.GetValueStore().InheritanceAncestor); + } + + [Fact] + public void Clearing_Value_In_Parent_Updates_InheritanceAncestor() + { + var parent = new Class1(); + var child = new Class1 { Parent = parent }; + var grandchild = new Class1 { Parent = child }; + + parent.Foo = "changed"; + parent.ClearValue(Class1.FooProperty); + + Assert.Null(parent.GetValueStore().InheritanceAncestor); + Assert.Null(child.GetValueStore().InheritanceAncestor); + Assert.Null(grandchild.GetValueStore().InheritanceAncestor); + } + + [Fact] + public void Clear_Value_In_Parent_Doesnt_Update_Grandchild_InheritanceAncestor_If_Child_Has_Value_Set() + { + var parent = new Class1(); + var child = new Class1 { Parent = parent }; + var grandchild = new Class1 { Parent = child }; + + child.Foo = "foochanged"; + parent.Foo = "changed"; + parent.ClearValue(Class1.FooProperty); + + Assert.Null(parent.GetValueStore().InheritanceAncestor); + Assert.Null(child.GetValueStore().InheritanceAncestor); + Assert.Same(child.GetValueStore(), grandchild.GetValueStore().InheritanceAncestor); + } + + [Fact] + public void Clearing_Value_In_Child_Updates_InheritanceAncestor() + { + var parent = new Class1(); + var child = new Class1 { Parent = parent }; + var grandchild = new Class1 { Parent = child }; + + parent.Foo = "changed"; + child.Foo = "foochanged"; + child.ClearValue(Class1.FooProperty); + + var parentStore = parent.GetValueStore(); + Assert.Null(parentStore.InheritanceAncestor); + Assert.Same(parentStore, child.GetValueStore().InheritanceAncestor); + Assert.Same(parentStore, grandchild.GetValueStore().InheritanceAncestor); + } + + [Fact] + public void Adding_Child_Sets_InheritanceAncestor() + { + var parent = new Class1(); + var child = new Class1(); + var grandchild = new Class1 { Parent = child }; + + parent.Foo = "changed"; + child.Parent = parent; + + var parentStore = parent.GetValueStore(); + Assert.Null(parentStore.InheritanceAncestor); + Assert.Same(parentStore, child.GetValueStore().InheritanceAncestor); + Assert.Same(parentStore, grandchild.GetValueStore().InheritanceAncestor); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", "foodefault", inherits: true); + + public string Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public Class1? Parent + { + get { return (Class1?)InheritanceParent; } + set { InheritanceParent = value; } + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index 9048b488b6..72df072ea6 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -292,9 +292,15 @@ namespace Avalonia.Base.UnitTests.Styling private class ActivatorSink : IStyleActivatorSink { - public ActivatorSink(IStyleActivator source) => source.Subscribe(this); + public ActivatorSink(IStyleActivator source) + { + source.Subscribe(this); + Active = source.GetIsActive(); + } + + public bool Active { get; private set; } - public void OnNext(bool value, int tag) => Active = value; + public void OnNext(bool value) => Active = value; } } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs index b57a024f41..dc31d3d3ec 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs +++ b/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("foo"); var descriptor = InstancedBinding.OneWay(subject); - var binding = Mock.Of(x => x.Initiate(control, TextBlock.TextProperty, null, false) == descriptor); + var binding = Mock.Of(x => x.Initiate(control, TextBlock.TagProperty, null, false) == descriptor); var style = Mock.Of(); - 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(); var setter = new Setter(TextBlock.TagProperty, binding); - setter.Instance(control).Start(false); + Apply(setter, control); Assert.Equal(null, control.Text); } @@ -60,133 +63,463 @@ namespace Avalonia.Base.UnitTests.Styling var style = Mock.Of(); var setter = new Setter(Decorator.ChildProperty, template); - setter.Instance(control).Start(false); + Apply(setter, control); Assert.IsType(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()) + { + 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("foo"); + var style = new Style(x => x.Is()) + { + 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("foo"); + var style = new Style(x => x.Is().Class("foo")) + { + Setters = + { + new Setter(TextBlock.TextProperty, source.ToBinding()), + } + }; + + Assert.Throws(() => 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().Class("foo")) + { + Setters = + { + new Setter(TextBlock.TextProperty, "foo"), + } + }; + + Assert.Throws(() => Apply(style, control)); + } + [Fact] public void Does_Not_Call_Converter_ConvertBack_On_OneWay_Binding() { - var control = new Decorator { Name = "foo" }; - var style = Mock.Of(); + 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().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()) + { + 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().Class("foo")) + { + Setters = + { + new Setter(Control.TagProperty, "foo"), + }, + }; + var activator = new Subject(); + var raised = 0; - var instance = setter.Instance(control); - instance.Start(true); - instance.Activate(); + control.PropertyChanged += (s, e) => + { + Assert.Equal(Border.TagProperty, e.Property); + Assert.Equal(BindingPriority.StyleTrigger, e.Priority); + ++raised; + }; - Assert.Equal("foo", control.Tag); - Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority); + Apply(style, control); + + 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 + { + DataContext = "foo", + }; + + var style = new Style(x => x.OfType()) { - Source = source, - Path = nameof(source.Foo), - }); + Setters = + { + new Setter(Control.TagProperty, new Binding()), + }, + }; - setter.Instance(control).Start(false); + var raised = 0; - Assert.Equal("foo", control.Tag); - Assert.Equal(BindingPriority.Style, control.GetDiagnostic(TextBlock.TagProperty).Priority); + 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 + { + Classes = { "foo" }, + DataContext = "foo", + }; + + var style = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Control.TagProperty, new Binding()), + }, + }; + + 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 Direct_Property_Setter_With_TwoWay_Binding_Should_Update_Source() + { + using var app = UnitTestApplication.Start(TestServices.MockThreadingInterface); + var data = new Data { Foo = "foo" }; + var control = new TextBox { - 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()) + { + Setters = + { + new Setter + { + Property = TextBox.TextProperty, + Value = new Binding + { + Path = "Foo", + Mode = BindingMode.TwoWay + } + } + }, + }; - Assert.Equal("foo", control.Tag); - Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority); + Apply(style, control); + Assert.Equal("foo", control.Text); + + control.Text = "bar"; + Assert.Equal("bar", data.Foo); + } + + [Fact] + public void Styled_Property_Setter_With_TwoWay_Binding_Should_Update_Source() + { + var data = new Data { Bar = Brushes.Red }; + var control = new Border + { + DataContext = data, + }; + + var style = new Style(x => x.OfType()) + { + 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); + } + + [Fact] + public void Non_Active_Styled_Property_Binding_Should_Be_Unsubscribed() + { + var data = new Data { Bar = Brushes.Red }; + var control = new Border + { + DataContext = data, + }; + + var style1 = new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Border.BackgroundProperty, + Value = new Binding("Bar"), + } + }, + }; + + var style2 = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter + { + Property = Border.BackgroundProperty, + Value = Brushes.Green, + } + }, + }; + + Apply(style1, control); + Apply(style2, control); + + // `style1` is initially active. + Assert.Equal(Brushes.Red, control.Background); + Assert.Equal(1, data.PropertyChangedSubscriptionCount); + + // Activate `style2`. + control.Classes.Add("foo"); + Assert.Equal(Brushes.Green, control.Background); + + // The binding from `style1` is now inactive and so should be unsubscribed. + Assert.Equal(0, data.PropertyChangedSubscriptionCount); } [Fact] - public void Disposing_Setter_Should_Preserve_LocalValue() + public void Non_Active_Styled_Property_Setter_With_TwoWay_Binding_Should_Not_Update_Source() { - var control = new Canvas(); - var setter = new Setter(TextBlock.TagProperty, "foo"); + var data = new Data { Bar = Brushes.Red }; + var control = new Border + { + DataContext = data, + }; - var instance = setter.Instance(control); - instance.Start(true); - instance.Activate(); + var style1 = new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Border.BackgroundProperty, + Value = new Binding + { + Path = "Bar", + Mode = BindingMode.TwoWay + } + } + }, + }; - control.Tag = "bar"; + var style2 = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter + { + Property = Border.BackgroundProperty, + Value = Brushes.Green, + } + }, + }; + + Apply(style1, control); + Apply(style2, control); - instance.Dispose(); + // `style1` is initially active. + Assert.Equal(Brushes.Red, control.Background); - Assert.Equal("bar", control.Tag); + // Activate `style2`. + control.Classes.Add("foo"); + Assert.Equal(Brushes.Green, control.Background); + + // The two-way binding from `style1` is now inactive and so should not write back to + // the DataContext. + Assert.Equal(Brushes.Red, data.Bar); } [Fact] - public void Disposing_Binding_Setter_Should_Preserve_LocalValue() + public void Styled_Property_Setter_With_TwoWay_Binding_Updates_Source_When_Made_Active() { - 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), - }); + Classes = { "foo" }, + DataContext = data, + }; - var instance = setter.Instance(control); - instance.Start(true); - instance.Activate(); + var style1 = new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Border.BackgroundProperty, + Value = new Binding + { + Path = "Bar", + Mode = BindingMode.TwoWay + } + } + }, + }; + + var style2 = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter + { + Property = Border.BackgroundProperty, + Value = Brushes.Green, + } + }, + }; + + Apply(style1, control); + Apply(style2, control); + + // `style2` is initially active. + Assert.Equal(Brushes.Green, control.Background); + + // Deactivate `style2`. + control.Classes.Remove("foo"); + Assert.Equal(Brushes.Red, control.Background); + + // The two-way binding from `style1` is now active and so should write back to the + // DataContext. + control.Background = Brushes.Blue; + Assert.Equal(Brushes.Blue, data.Bar); + } - control.Tag = "bar"; + private void Apply(Style style, Control control) + { + style.TryAttach(control, null); + } - instance.Dispose(); + private void Apply(Setter setter, Control control) + { + var style = new Style(x => x.Is()) + { + Setters = { setter }, + }; + + Apply(style, control); + } - Assert.Equal("bar", control.Tag); + private class Data : NotifyingBase + { + public string? Foo { get; set; } + public IBrush? Bar { get; set; } } private class TestConverter : IValueConverter diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs index f5b3bb40a3..e69eae43f0 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs @@ -33,10 +33,16 @@ namespace Avalonia.Base.UnitTests.Styling private readonly IStyleActivator _source; public ObservableAdapter(IStyleActivator source) => _source = source; + protected override void Initialize() => _source.Subscribe(this); protected override void Deinitialize() => _source.Unsubscribe(this); + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(_source.GetIsActive()); + } - void IStyleActivatorSink.OnNext(bool value, int tag) + void IStyleActivatorSink.OnNext(bool value) { PublishNext(value); } diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs index f2dfe66054..b7455f9b3f 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using Avalonia.Animation; +using Avalonia.Base.UnitTests.Animation; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Diagnostics; using Avalonia.Styling; using Avalonia.UnitTests; using Moq; @@ -146,7 +149,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] @@ -226,6 +229,75 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Equal(new[] { "foodefault", "Bar" }, values); } + [Fact] + public void Later_Styles_Should_Override_Earlier_4() + { + Styles styles = new Styles + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.FooProperty, "foo1"), + }, + }, + + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.FooProperty, "foo2"), + new Setter(Class1.DoubleProperty, 123.4), + }, + } + }; + + var target = new Class1(); + styles.TryAttach(target, null); + target.Classes.Add("foo"); + + Assert.Equal("foo2", target.Foo); + Assert.Equal(123.4, target.Double); + } + + [Fact] + public void Later_Styles_Should_Override_Earlier_With_Begin_End_Styling() + { + Styles styles = new Styles + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.FooProperty, "foo1"), + new Setter(Class1.DoubleProperty, 123.4), + }, + }, + + new Style(x => x.OfType().Class("foo").Class("bar")) + { + Setters = + { + new Setter(Class1.FooProperty, "foo2"), + }, + }, + }; + + var target = new Class1(); + target.GetValueStore().BeginStyling(); + styles.TryAttach(target, null); + target.GetValueStore().EndStyling(); + target.Classes.Add("bar"); + target.Classes.Add("foo"); + + Assert.Equal("foo2", target.Foo); + Assert.Equal(123.4, target.Double); + + target.Classes.Remove("foo"); + + Assert.Equal(0, target.Double); + } + [Fact] public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Attach() { @@ -490,9 +562,9 @@ namespace Avalonia.Base.UnitTests.Styling }; var target = new Class1(); - target.BeginBatchUpdate(); + target.GetValueStore().BeginStyling(); styles.TryAttach(target, null); - target.EndBatchUpdate(); + target.GetValueStore().EndStyling(); Assert.NotNull(target.Child); Assert.Equal(1, instantiationCount); @@ -702,6 +774,28 @@ namespace Avalonia.Base.UnitTests.Styling } } + [Fact] + public void DetachStyles_Should_Detach_Activator() + { + Style style = new Style(x => x.OfType().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() { @@ -771,6 +865,56 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Throws(() => parent.Children.Add(nested)); } + [Fact] + public void Animations_Should_Be_Activated_And_Deactivated() + { + Style style = new Style(x => x.OfType().Class("foo")) + { + Animations = + { + new Avalonia.Animation.Animation + { + Duration = TimeSpan.FromSeconds(1), + Children = + { + new KeyFrame + { + Setters = + { + new Setter { Property = Class1.DoubleProperty, Value = 5.0 } + }, + }, + new KeyFrame + { + Setters = + { + new Setter { Property = Class1.DoubleProperty, Value = 10.0 } + }, + Cue = new Cue(1d) + } + }, + } + } + }; + + var clock = new TestClock(); + var target = new Class1 { Clock = clock }; + + style.TryAttach(target, null); + + Assert.Equal(0.0, target.Double); + + target.Classes.Add("foo"); + clock.Step(TimeSpan.Zero); + Assert.Equal(5.0, target.Double); + + clock.Step(TimeSpan.FromSeconds(0.5)); + Assert.Equal(7.5, target.Double); + + target.Classes.Remove("foo"); + Assert.Equal(0.0, target.Double); + } + private class Class1 : Control { public static readonly StyledProperty FooProperty = @@ -779,6 +923,9 @@ namespace Avalonia.Base.UnitTests.Styling public static readonly StyledProperty ChildProperty = AvaloniaProperty.Register(nameof(Child)); + public static readonly StyledProperty DoubleProperty = + AvaloniaProperty.Register(nameof(Double)); + public string Foo { get { return GetValue(FooProperty); } @@ -791,6 +938,12 @@ namespace Avalonia.Base.UnitTests.Styling set => SetValue(ChildProperty, value); } + public double Double + { + get => GetValue(DoubleProperty); + set => SetValue(DoubleProperty, value); + } + protected override Size MeasureOverride(Size availableSize) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs index bb4d590060..c01e22347b 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests.cs +++ b/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(new Styler()); + root.Child = child; Assert.Throws(() => 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(); - ((IStyleable)child).StyleApplied(styleInstance.Object); - - root.Child = null; + Styles = + { + new Style(x => x.OfType()) + { + 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", @@ -489,6 +470,57 @@ namespace Avalonia.Base.UnitTests.Styling called); } + + [Fact] + public void DataContext_Notifications_Should_Be_Called_In_Correct_Order_When_Setting_Parent() + { + var root = new TestStackPanel + { + Name = "root", + DataContext = "foo", + }; + + var children = new[] + { + new TestControl + { + Name = "a1", + Child = new TestControl + { + Name = "b1", + } + }, + new TestControl + { + Name = "a2", + DataContext = "foo", + }, + }; + + var called = new List(); + + foreach (IDataContextEvents c in new[] { children[0], children[0].Child, children[1] }) + { + c.DataContextBeginUpdate += (s, e) => called.Add("begin " + ((StyledElement)s).Name); + c.DataContextChanged += (s, e) => called.Add("changed " + ((StyledElement)s).Name); + c.DataContextEndUpdate += (s, e) => called.Add("end " + ((StyledElement)s).Name); + } + + root.Children.AddRange(children); + + Assert.Equal( + new[] + { + "begin a1", + "begin b1", + "changed a1", + "changed b1", + "end b1", + "end a1", + }, + called); + } + [Fact] public void Resources_Owner_Is_Set() { diff --git a/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs new file mode 100644 index 0000000000..362eee8035 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Utilities +{ + public class AvaloniaPropertyDictionaryTests + { + private static AvaloniaProperty[] TestProperties; + + static AvaloniaPropertyDictionaryTests() + { + TestProperties = new AvaloniaProperty[100]; + + for (var i = 0; i < 100; ++i) + { + TestProperties[i] = new StyledProperty( + $"Test{i}", + typeof(AvaloniaPropertyDictionaryTests), + new StyledPropertyMetadata()); + } + + Shuffle(TestProperties, 42); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Property_Indexer_Finds_Value(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + var property = TestProperties[index]; + var result = target[property]; + + Assert.Equal($"Value{index}", result); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Property_Indexer_Throws_If_Value_Not_Found(int count) + { + var target = CreateTarget(count); + var index = count; + var property = TestProperties[index]; + + Assert.Throws(() => target[property]); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Property_Indexer_Adds_New_Value(int count) + { + var target = CreateTarget(count); + var index = count; + var property = TestProperties[index]; + + target[property] = "new"; + + Assert.Equal("new", target[property]); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Property_Indexer_Sets_Existing_Value(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + var property = TestProperties[index]; + + Assert.Equal($"Value{index}", target[property]); + + target[property] = "new"; + + Assert.Equal("new", target[property]); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Int_Indexer_Finds_Value(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + var result = target[index]; + + Assert.NotNull(result); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Int_Indexer_Throws_If_Index_Out_Of_Range(int count) + { + var target = CreateTarget(count); + var index = count; + + Assert.Throws(() => target[index]); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Add_Adds_New_Value(int count) + { + var target = CreateTarget(count); + var index = count; + var property = TestProperties[index]; + + target.Add(property, "new"); + + Assert.Equal("new", target[property]); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Add_Throws_If_Key_Exists(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + var property = TestProperties[index]; + + Assert.Throws(() => target.Add(property, "new")); + } + + + [Theory] + [MemberData(nameof(Counts))] + public void ContainsKey_Returns_True_If_Value_Exists(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + var property = TestProperties[index]; + + Assert.True(target.ContainsKey(property)); + } + + [Theory] + [MemberData(nameof(Counts))] + public void ContainsKey_Returns_False_If_Value_Does_Not_Exist(int count) + { + var target = CreateTarget(count); + var index = count; + var property = TestProperties[index]; + + Assert.False(target.ContainsKey(property)); + } + + [Theory] + [MemberData(nameof(Counts))] + public void GetKeyValue_Finds_Value(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + + target.GetKeyValue(index, out var property, out var value); + + Assert.NotNull(property); + Assert.NotNull(value); + } + + [Theory] + [MemberData(nameof(Counts))] + public void GetKeyValue_Throws_If_Index_Out_Of_Range(int count) + { + var target = CreateTarget(count); + var index = count; + + Assert.Throws(() => target.GetKeyValue(index, out var _, out var _)); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Remove_Removes_Value(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + var property = TestProperties[index]; + + Assert.True(target.Remove(property)); + Assert.False(target.ContainsKey(property)); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Remove_Returns_False_If_Value_Not_Present(int count) + { + var target = CreateTarget(count); + var index = count; + var property = TestProperties[index]; + + Assert.False(target.Remove(property)); + } + + [Theory] + [MemberData(nameof(Counts))] + public void Remove_Returns_Existing_Value(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + var property = TestProperties[index]; + + Assert.True(target.Remove(property, out var value)); + Assert.Equal($"Value{index}", value); + } + + [Theory] + [MemberData(nameof(Counts))] + public void TryAdd_Adds_New_Value(int count) + { + var target = CreateTarget(count); + var index = count; + var property = TestProperties[index]; + + Assert.True(target.TryAdd(property, "new")); + + Assert.Equal("new", target[property]); + } + + [Theory] + [MemberData(nameof(Counts))] + public void TryAdd_Returns_False_If_Key_Exists(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + var property = TestProperties[index]; + + Assert.False(target.TryAdd(property, "new")); + } + + [Theory] + [MemberData(nameof(Counts))] + public void TryGetValue_Finds_Value(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count / 2; + var property = TestProperties[index]; + + Assert.True(target.TryGetValue(property, out var value)); + Assert.Equal($"Value{index}", value); + } + + [Theory] + [MemberData(nameof(Counts))] + public void TryGetValue_Returns_False_If_Key_Does_Not_Exist(int count) + { + if (count == 0) + return; + + var target = CreateTarget(count); + var index = count; + var property = TestProperties[index]; + + Assert.False(target.TryGetValue(property, out var value)); + Assert.Null(value); + } + + public static TheoryData Counts() + { + var result = new TheoryData(); + result.Add(0); + result.Add(1); + result.Add(10); + result.Add(13); + result.Add(50); + result.Add(72); + return result; + } + + private static AvaloniaPropertyDictionary CreateTarget(int items) + { + var result = new AvaloniaPropertyDictionary(); + + for (var i = 0; i < items; ++i) + result.Add(TestProperties[i], $"Value{i}"); + + return result; + } + + private static void Shuffle(T[] array, int seed) + { + var rng = new Random(seed); + + int n = array.Length; + while (n > 1) + { + int k = rng.Next(n--); + T temp = array[n]; + array[n] = array[k]; + array[k] = temp; + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/VisualTests.cs b/tests/Avalonia.Base.UnitTests/VisualTests.cs index 382001229b..8dff5a6f61 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTests.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTests.cs @@ -281,13 +281,17 @@ namespace Avalonia.Base.UnitTests { var target = new Decorator(); var root = new TestRoot { Child = target, DataContext = "foo" }; - var called = false; + var called = 0; LogCallback checkLogMessage = (level, area, src, mt, pv) => { if (level >= Avalonia.Logging.LogEventLevel.Warning) { - called = true; + Assert.Equal("Error in binding to {Target}.{Property}: {Message}", mt); + Assert.Same(target, pv[0]); + Assert.Equal(Decorator.TagProperty, pv[1]); + Assert.Equal("Could not find a matching property accessor for 'Foo' on 'foo'", pv[2]); + ++called; } }; @@ -296,7 +300,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Decorator.TagProperty, new Binding("Foo")); } - Assert.True(called); + Assert.Equal(1, called); } [Fact] diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 754a1d6a24..941d377a17 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -4,6 +4,7 @@ Exe false + diff --git a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs index 51b52d6130..eacf73ac94 100644 --- a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs @@ -55,6 +55,42 @@ namespace Avalonia.Benchmarks.Layout Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); } + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateControl() + { + var control = new Control(); + + _root.Child = control; + + _root.LayoutManager.ExecuteLayoutPass(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateDecorator() + { + var control = new Decorator(); + + _root.Child = control; + + _root.LayoutManager.ExecuteLayoutPass(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateScrollViewer() + { + var control = new ScrollViewer(); + + _root.Child = control; + + _root.LayoutManager.ExecuteLayoutPass(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + } + [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public void CreateButton() diff --git a/tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs b/tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs deleted file mode 100644 index 0c9bcf412f..0000000000 --- a/tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs +++ /dev/null @@ -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