From 4a752c3f48f355a442694574fa1e068aa0cfdf10 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 09:39:22 +0200 Subject: [PATCH 1/9] Get non-animated change information. - Pass information for all property changes to `OnPropertyChangedCore`; whether they result in an effective value change or not - Added `GetBaseValue` to get a value with a specified priority - Change the signature of `OnPropertyChanged` again to take an `AvaloniaPropertyChangedEventArgs` --- src/Avalonia.Animation/Animatable.cs | 18 +- src/Avalonia.Base/AvaloniaObject.cs | 198 +++++++++++------- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 59 ++++++ src/Avalonia.Base/AvaloniaProperty.cs | 7 + .../AvaloniaPropertyChangedEventArgs.cs | 23 +- .../AvaloniaPropertyChangedEventArgs`1.cs | 24 ++- src/Avalonia.Base/DirectPropertyBase.cs | 5 + src/Avalonia.Base/IAvaloniaObject.cs | 13 ++ .../PropertyStore/BindingEntry.cs | 20 +- .../PropertyStore/ConstantValueEntry.cs | 14 +- src/Avalonia.Base/PropertyStore/IValue.cs | 6 +- src/Avalonia.Base/PropertyStore/IValueSink.cs | 6 +- .../PropertyStore/LocalValueEntry.cs | 11 +- .../PropertyStore/PriorityValue.cs | 161 +++++++++----- src/Avalonia.Base/StyledPropertyBase.cs | 7 + src/Avalonia.Base/ValueStore.cs | 69 +++--- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 +- src/Avalonia.Controls/Button.cs | 12 +- src/Avalonia.Controls/ButtonSpinner.cs | 12 +- src/Avalonia.Controls/Calendar/DatePicker.cs | 12 +- src/Avalonia.Controls/Expander.cs | 12 +- .../WindowNotificationManager.cs | 12 +- src/Avalonia.Controls/Primitives/ScrollBar.cs | 20 +- src/Avalonia.Controls/Primitives/Track.cs | 12 +- src/Avalonia.Controls/ProgressBar.cs | 16 +- .../Repeater/ItemsRepeater.cs | 28 +-- src/Avalonia.Controls/Slider.cs | 12 +- src/Avalonia.Controls/Window.cs | 12 +- src/Avalonia.Input/InputElement.cs | 12 +- src/Avalonia.Layout/StackLayout.cs | 6 +- src/Avalonia.Layout/UniformGridLayout.cs | 35 ++-- .../Styling/PropertySetterBindingInstance.cs | 2 +- src/Avalonia.Visuals/Media/DrawingImage.cs | 10 +- .../AvaloniaObjectTests_GetValue.cs | 51 +++++ .../AvaloniaObjectTests_OnPropertyChanged.cs | 142 +++++++++++++ .../AvaloniaPropertyTests.cs | 19 ++ .../PriorityValueTests.cs | 98 ++++++++- .../Xaml/InitializationOrderTracker.cs | 6 +- .../Rendering/ImmediateRendererTests.cs | 8 +- 39 files changed, 831 insertions(+), 361 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index fa1e955153..324ff06452 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -62,30 +62,26 @@ namespace Avalonia.Animation } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation) + if (_transitions is null || _previousTransitions is null || change.Priority == BindingPriority.Animation) return; // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). foreach (var transition in _transitions) { - if (transition.Property == property) + if (transition.Property == change.Property) { - if (_previousTransitions.TryGetValue(property, out var dispose)) + if (_previousTransitions.TryGetValue(change.Property, out var dispose)) dispose.Dispose(); var instance = transition.Apply( this, Clock ?? Avalonia.Animation.Clock.GlobalClock, - oldValue.GetValueOrDefault(), - newValue.GetValueOrDefault()); + change.OldValue.GetValueOrDefault(), + change.NewValue.GetValueOrDefault()); - _previousTransitions[property] = instance; + _previousTransitions[change.Property] = instance; return; } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index ed36e6da43..546ab78e31 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -259,6 +259,21 @@ namespace Avalonia return registered.InvokeGetter(this); } + /// + public Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + if (_values is object && + _values.TryGetValue(property, maxPriority, out var value)) + { + return value; + } + + return default; + } + /// /// Checks whether a is animating. /// @@ -458,29 +473,43 @@ namespace Avalonia return _propertyChanged?.GetInvocationList(); } - void IValueSink.ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property); - newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property)); + var property = (StyledPropertyBase)change.Property; - LogIfError(property, newValue); + LogIfError(property, change.NewValue); - if (!EqualityComparer.Default.Equals(oldValue.Value, newValue.Value)) + // 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) { - RaisePropertyChanged(property, oldValue, newValue, priority); + change.SetOldValue(GetInheritedOrDefault(property)); + } - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "{Property} changed from {$Old} to {$Value} with priority {Priority}", - property, - oldValue, - newValue, - (BindingPriority)priority); + 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)?.Log( + LogArea.Property, + this, + "{Property} changed from {$Old} to {$Value} with priority {Priority}", + property, + change.OldValue, + change.NewValue, + change.Priority); + } } } @@ -489,7 +518,13 @@ namespace Avalonia IPriorityValueEntry entry, Optional oldValue) { - ((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default); + var change = new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + default, + BindingPriority.Unset); + ((IValueSink)this).ValueChanged(change); } /// @@ -575,15 +610,20 @@ namespace Avalonia /// /// Called when a avalonia property changes on the object. /// - /// The property whose value has changed. - /// The old value of the property. - /// The new value of the property. - /// The priority of the new value. - protected virtual void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + /// The property change details. + protected virtual void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + { + if (change.IsEffectiveValueChange) + { + OnPropertyChanged(change); + } + } + + /// + /// Called when a avalonia property changes on the object. + /// + /// The property change details. + protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { } @@ -600,57 +640,12 @@ namespace Avalonia BindingValue newValue, BindingPriority priority = BindingPriority.LocalValue) { - property = property ?? throw new ArgumentNullException(nameof(property)); - - VerifyAccess(); - - property.Notifying?.Invoke(this, true); - - try - { - AvaloniaPropertyChangedEventArgs e = null; - var hasChanged = property.HasChangedSubscriptions; - - if (hasChanged || _propertyChanged != null) - { - e = new AvaloniaPropertyChangedEventArgs( - this, - property, - oldValue, - newValue, - priority); - } - - OnPropertyChanged(property, oldValue, newValue, priority); - - if (hasChanged) - { - property.NotifyChanged(e); - } - - _propertyChanged?.Invoke(this, e); - - if (_inpcChanged != null) - { - var inpce = new PropertyChangedEventArgs(property.Name); - _inpcChanged(this, inpce); - } - - if (property.Inherits && _inheritanceChildren != null) - { - foreach (var child in _inheritanceChildren) - { - child.InheritedPropertyChanged( - property, - oldValue, - newValue.ToOptional()); - } - } - } - finally - { - property.Notifying?.Invoke(this, false); - } + RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + newValue, + priority)); } /// @@ -689,7 +684,9 @@ namespace Avalonia return property.GetDefaultValue(GetType()); } - private T GetValueOrInheritedOrDefault(StyledPropertyBase property) + private T GetValueOrInheritedOrDefault( + StyledPropertyBase property, + BindingPriority maxPriority = BindingPriority.Animation) { var o = this; var inherits = property.Inherits; @@ -699,7 +696,7 @@ namespace Avalonia { var values = o._values; - if (values?.TryGetValue(property, out value) == true) + if (values?.TryGetValue(property, maxPriority, out value) == true) { return value; } @@ -715,6 +712,45 @@ namespace Avalonia return property.GetDefaultValue(GetType()); } + protected internal void RaisePropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + VerifyAccess(); + + 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 + { + change.Property.Notifying?.Invoke(this, false); + } + } + /// /// Sets the value of a direct property. /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 4fc65a3ed4..173c5c1a94 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -448,6 +448,65 @@ namespace Avalonia }; } + /// + /// Gets an base value. + /// + /// 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 + /// . Note that this method does not return + /// property values that come from inherited or default values. + /// + /// For direct properties returns . + /// + public static object GetBaseValue( + this IAvaloniaObject target, + AvaloniaProperty property, + BindingPriority maxPriority) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property.RouteGetBaseValue(target, maxPriority); + } + + /// + /// Gets an base value. + /// + /// 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 + /// . Note that this method does not return property values + /// that come from inherited or default values. + /// + /// For direct properties returns + /// . + /// + public static Optional GetBaseValue( + this IAvaloniaObject target, + AvaloniaProperty property, + BindingPriority maxPriority) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property switch + { + StyledPropertyBase styled => target.GetBaseValue(styled, maxPriority), + DirectPropertyBase direct => target.GetValue(direct), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") + }; + } + /// /// Sets a value. /// diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 74d7039751..daa7191cc5 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -496,6 +496,13 @@ namespace Avalonia /// The object instance. internal abstract object RouteGetValue(IAvaloniaObject o); + /// + /// Routes an untyped GetBaseValue call to a typed call. + /// + /// The object instance. + /// The maximum priority for the value. + internal abstract object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority); + /// /// Routes an untyped SetValue call to a typed call. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index 0f09747865..c1a2832fde 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -16,6 +16,7 @@ namespace Avalonia { Sender = sender; Priority = priority; + IsEffectiveValueChange = true; } /// @@ -35,19 +36,11 @@ namespace Avalonia /// /// Gets the old value of the property. /// - /// - /// The old value of the property or if the - /// property previously had no value. - /// public object? OldValue => GetOldValue(); /// /// Gets the new value of the property. /// - /// - /// The new value of the property or if the - /// property previously had no value. - /// public object? NewValue => GetNewValue(); /// @@ -58,6 +51,20 @@ 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 recieves 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 signalled + /// has not resulted in a change to the property value on the object. + /// + public 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 fca32b4ffc..054bf93b3a 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.Data; #nullable enable @@ -6,7 +5,7 @@ using Avalonia.Data; namespace Avalonia { /// - /// Provides information for a avalonia property change. + /// Provides information for an Avalonia property change. /// public class AvaloniaPropertyChangedEventArgs : AvaloniaPropertyChangedEventArgs { @@ -42,19 +41,28 @@ namespace Avalonia /// /// Gets the old value of the property. /// - /// - /// 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. /// - /// - /// 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/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index d3b5277c53..0e65379abd 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -120,6 +120,11 @@ namespace Avalonia return o.GetValue(this); } + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + return o.GetValue(this); + } + /// internal override IDisposable? RouteSetValue( IAvaloniaObject o, diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 867249bf0e..0452f77d4c 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -41,6 +41,19 @@ namespace Avalonia /// The value. T GetValue(DirectPropertyBase property); + /// + /// Gets an base value. + /// + /// The type of the property. + /// The property. + /// The maximum priority for the value. + /// + /// Gets the value of the property, if set on this object with a priority equal or lower to + /// , otherwise . Note that + /// this method does not return property values that come from inherited or default values. + /// + Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority); + /// /// Checks whether a is animating. /// diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 3249b31d66..0d563947e7 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -22,6 +22,7 @@ namespace Avalonia.PropertyStore private readonly IAvaloniaObject _owner; private IValueSink _sink; private IDisposable? _subscription; + private Optional _value; public BindingEntry( IAvaloniaObject owner, @@ -40,18 +41,21 @@ namespace Avalonia.PropertyStore public StyledPropertyBase Property { get; } public BindingPriority Priority { get; } public IObservable> Source { get; } - public Optional Value { get; private set; } - Optional IValue.Value => Value.ToObject(); - BindingPriority IValue.ValuePriority => Priority; + Optional IValue.GetValue() => _value.ToObject(); + + public Optional GetValue(BindingPriority maxPriority) + { + return Priority >= maxPriority ? _value : Optional.Empty; + } public void Dispose() { _subscription?.Dispose(); _subscription = null; - _sink.Completed(Property, this, Value); + _sink.Completed(Property, this, _value); } - public void OnCompleted() => _sink.Completed(Property, this, Value); + public void OnCompleted() => _sink.Completed(Property, this, _value); public void OnError(Exception error) { @@ -94,14 +98,14 @@ namespace Avalonia.PropertyStore return; } - var old = Value; + var old = _value; if (value.Type != BindingValueType.DataValidationError) { - Value = value.ToOptional(); + _value = value.ToOptional(); } - _sink.ValueChanged(Property, Priority, old, value); + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(_owner, Property, old, value, Priority)); } } } diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index aa054c46ff..46f6f9a137 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -13,6 +13,7 @@ namespace Avalonia.PropertyStore internal class ConstantValueEntry : IPriorityValueEntry, IDisposable { private IValueSink _sink; + private Optional _value; public ConstantValueEntry( StyledPropertyBase property, @@ -21,18 +22,21 @@ namespace Avalonia.PropertyStore IValueSink sink) { Property = property; - Value = value; + _value = value; Priority = priority; _sink = sink; } public StyledPropertyBase Property { get; } public BindingPriority Priority { get; } - public Optional Value { get; } - Optional IValue.Value => Value.ToObject(); - BindingPriority IValue.ValuePriority => Priority; + Optional IValue.GetValue() => _value.ToObject(); - public void Dispose() => _sink.Completed(Property, this, Value); + public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) + { + return Priority >= maxPriority ? _value : Optional.Empty; + } + + public void Dispose() => _sink.Completed(Property, this, _value); public void Reparent(IValueSink sink) => _sink = sink; } } diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs index 0ce7fb8308..249cfc360c 100644 --- a/src/Avalonia.Base/PropertyStore/IValue.cs +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -9,8 +9,8 @@ namespace Avalonia.PropertyStore /// internal interface IValue { - Optional Value { get; } - BindingPriority ValuePriority { get; } + Optional GetValue(); + BindingPriority Priority { get; } } /// @@ -19,6 +19,6 @@ namespace Avalonia.PropertyStore /// The property type. internal interface IValue : IValue { - new Optional Value { get; } + Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation); } } diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs index 9012a985ac..3a1e9731d8 100644 --- a/src/Avalonia.Base/PropertyStore/IValueSink.cs +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -9,11 +9,7 @@ namespace Avalonia.PropertyStore /// internal interface IValueSink { - void ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue); + void ValueChanged(AvaloniaPropertyChangedEventArgs change); void Completed( StyledPropertyBase property, diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 22258390da..59c017bc09 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -14,9 +14,14 @@ namespace Avalonia.PropertyStore private T _value; public LocalValueEntry(T value) => _value = value; - public Optional Value => _value; - public BindingPriority ValuePriority => BindingPriority.LocalValue; - Optional IValue.Value => Value.ToObject(); + 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; } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index affb20f334..5e223cad60 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Avalonia.Data; #nullable enable @@ -24,6 +25,7 @@ namespace Avalonia.PropertyStore private readonly List> _entries = new List>(); private readonly Func? _coerceValue; private Optional _localValue; + private Optional _value; public PriorityValue( IAvaloniaObject owner, @@ -50,11 +52,13 @@ namespace Avalonia.PropertyStore { existing.Reparent(this); _entries.Add(existing); + + var v = existing.GetValue(); - if (existing.Value.HasValue) + if (v.HasValue) { - Value = existing.Value; - ValuePriority = existing.Priority; + _value = v; + Priority = existing.Priority; } } @@ -65,18 +69,39 @@ namespace Avalonia.PropertyStore LocalValueEntry existing) : this(owner, property, sink) { - _localValue = existing.Value; - Value = _localValue; - ValuePriority = BindingPriority.LocalValue; + _value = _localValue = existing.GetValue(BindingPriority.LocalValue); + Priority = BindingPriority.LocalValue; } public StyledPropertyBase Property { get; } - public Optional Value { get; private set; } - public BindingPriority ValuePriority { get; private set; } + public BindingPriority Priority { get; private set; } = BindingPriority.Unset; public IReadOnlyList> Entries => _entries; - Optional IValue.Value => Value.ToObject(); + Optional IValue.GetValue() => _value.ToObject(); + + public void ClearLocalValue() + { + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + default, + default, + BindingPriority.LocalValue)); + } + + public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation) + { + if (Priority == BindingPriority.Unset) + { + return default; + } - public void ClearLocalValue() => UpdateEffectiveValue(); + if (Priority >= maxPriority) + { + return _value; + } + + return CalculateValue(maxPriority).Item1; + } public IDisposable? SetValue(T value, BindingPriority priority) { @@ -94,7 +119,13 @@ namespace Avalonia.PropertyStore result = entry; } - UpdateEffectiveValue(); + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + default, + value, + priority)); + return result; } @@ -106,20 +137,19 @@ namespace Avalonia.PropertyStore return binding; } - public void CoerceValue() => UpdateEffectiveValue(); + public void CoerceValue() => UpdateEffectiveValue(null); - void IValueSink.ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - if (priority == BindingPriority.LocalValue) + if (change.Priority == BindingPriority.LocalValue) { _localValue = default; } - UpdateEffectiveValue(); + if (change is AvaloniaPropertyChangedEventArgs c) + { + UpdateEffectiveValue(c); + } } void IValueSink.Completed( @@ -128,7 +158,16 @@ namespace Avalonia.PropertyStore Optional oldValue) { _entries.Remove((IPriorityValueEntry)entry); - UpdateEffectiveValue(); + + if (oldValue is Optional o) + { + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + o, + default, + entry.Priority)); + } } private int FindInsertPoint(BindingPriority priority) @@ -147,53 +186,73 @@ namespace Avalonia.PropertyStore return result; } - private void UpdateEffectiveValue() + public (Optional, BindingPriority) CalculateValue(BindingPriority maxPriority) { var reachedLocalValues = false; - var value = default(Optional); - if (_entries.Count > 0) + for (var i = _entries.Count - 1; i >= 0; --i) { - for (var i = _entries.Count - 1; i >= 0; --i) + var entry = _entries[i]; + + if (entry.Priority < maxPriority) + { + continue; + } + + if (!reachedLocalValues && + entry.Priority >= BindingPriority.LocalValue && + maxPriority <= BindingPriority.LocalValue && + _localValue.HasValue) + { + return (_localValue, BindingPriority.LocalValue); + } + + var entryValue = entry.GetValue(); + + if (entryValue.HasValue) { - var entry = _entries[i]; - - if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue) - { - reachedLocalValues = true; - - if (_localValue.HasValue) - { - value = _localValue; - ValuePriority = BindingPriority.LocalValue; - break; - } - } - - if (entry.Value.HasValue) - { - value = entry.Value; - ValuePriority = entry.Priority; - break; - } + return (entryValue, entry.Priority); } } - else if (_localValue.HasValue) + + if (!reachedLocalValues && + maxPriority <= BindingPriority.LocalValue && + _localValue.HasValue) { - value = _localValue; - ValuePriority = BindingPriority.LocalValue; + return (_localValue, BindingPriority.LocalValue); } + return (default, BindingPriority.Unset); + } + + private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs? change) + { + var (value, priority) = CalculateValue(BindingPriority.Animation); + if (value.HasValue && _coerceValue != null) { value = _coerceValue(_owner, value.Value); } - if (value != Value) + Priority = priority; + + if (value != _value) + { + var old = _value; + _value = value; + + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + old, + value, + Priority)); + } + else if (change is object) { - var old = Value; - Value = value; - _sink.ValueChanged(Property, ValuePriority, old, value); + change.MarkNonEffectiveValue(); + change.SetOldValue(default); + _sink.ValueChanged(change); } } } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 1f88bfb2aa..3e92c3bdf7 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -197,6 +197,13 @@ namespace Avalonia return o.GetValue(this); } + /// + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + var value = o.GetBaseValue(this, maxPriority); + return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; + } + /// internal override IDisposable RouteSetValue( IAvaloniaObject o, diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 104c06de0f..05e66f2e0a 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -37,7 +37,7 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { - return slot.ValuePriority < BindingPriority.LocalValue; + return slot.Priority < BindingPriority.LocalValue; } return false; @@ -47,21 +47,24 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { - return slot.Value.HasValue; + return slot.GetValue().HasValue; } return false; } - public bool TryGetValue(StyledPropertyBase property, out T value) + public bool TryGetValue( + StyledPropertyBase property, + BindingPriority maxPriority, + out T value) { if (_values.TryGetValue(property, out var slot)) { - var v = (IValue)slot; + var v = ((IValue)slot).GetValue(maxPriority); - if (v.Value.HasValue) + if (v.HasValue) { - value = v.Value.Value; + value = v.Value; return true; } } @@ -90,17 +93,22 @@ namespace Avalonia _values.AddValue(property, entry); result = entry.SetValue(value, priority); } - else if (priority == BindingPriority.LocalValue) - { - _values.AddValue(property, new LocalValueEntry(value)); - _sink.ValueChanged(property, priority, default, value); - } else { - var entry = new ConstantValueEntry(property, value, priority, this); - _values.AddValue(property, entry); - _sink.ValueChanged(property, priority, default, value); - result = entry; + var change = new AvaloniaPropertyChangedEventArgs(_owner, property, default, value, priority); + + if (priority == BindingPriority.LocalValue) + { + _values.AddValue(property, new LocalValueEntry(value)); + _sink.ValueChanged(change); + } + else + { + var entry = new ConstantValueEntry(property, value, priority, this); + _values.AddValue(property, entry); + _sink.ValueChanged(change); + result = entry; + } } return result; @@ -149,13 +157,14 @@ namespace Avalonia if (remove) { - var old = TryGetValue(property, out var value) ? value : default; + var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default; _values.Remove(property); - _sink.ValueChanged( + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, property, - BindingPriority.Unset, old, - BindingValue.Unset); + default, + BindingPriority.Unset)); } } } @@ -176,23 +185,20 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { + var slotValue = slot.GetValue(); return new Diagnostics.AvaloniaPropertyValue( property, - slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue, - slot.ValuePriority, + slotValue.HasValue ? slotValue.Value : AvaloniaProperty.UnsetValue, + slot.Priority, null); } return null; } - void IValueSink.ValueChanged( - StyledPropertyBase property, - BindingPriority priority, - Optional oldValue, - BindingValue newValue) + void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) { - _sink.ValueChanged(property, priority, oldValue, newValue); + _sink.ValueChanged(change); } void IValueSink.Completed( @@ -232,9 +238,14 @@ namespace Avalonia { if (priority == BindingPriority.LocalValue) { - var old = l.Value; + var old = l.GetValue(BindingPriority.LocalValue); l.SetValue(value); - _sink.ValueChanged(property, priority, old, value); + _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner, + property, + old, + value, + priority)); } else { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 844316741a..3fa773b71f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -767,7 +767,7 @@ namespace Avalonia.Controls /// /// ItemsProperty property changed handler. /// - /// AvaloniaPropertyChangedEventArgs. + /// AvaloniaPropertyChangedEventArgsdEventArgs. private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (!_areHandlersSuspended) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 8143bb1cf9..c171217642 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -313,17 +313,13 @@ namespace Avalonia.Controls IsPressed = false; } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == IsPressedProperty) + if (change.Property == IsPressedProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 2ac9319478..0717ab800a 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -205,17 +205,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == ButtonSpinnerLocationProperty) + if (change.Property == ButtonSpinnerLocationProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index b4e4ad1452..865b00cefc 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -512,17 +512,13 @@ namespace Avalonia.Controls base.OnTemplateApplied(e); } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == SelectedDateProperty) + if (change.Property == SelectedDateProperty) { - DataValidationErrors.SetError(this, newValue.Error); + DataValidationErrors.SetError(this, change.NewValue.Error); } } diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index e42d3ec1e5..43882b70c8 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -78,17 +78,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == ExpandDirectionProperty) + if (change.Property == ExpandDirectionProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index f1d8fe6763..62868f740a 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -138,17 +138,13 @@ namespace Avalonia.Controls.Notifications notificationControl.Close(); } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == PositionProperty) + if (change.Property == PositionProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index d48a9316e8..5ed75c97cf 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -123,24 +123,20 @@ namespace Avalonia.Controls.Primitives } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } else { - if (property == MinimumProperty || - property == MaximumProperty || - property == ViewportSizeProperty || - property == VisibilityProperty) + if (change.Property == MinimumProperty || + change.Property == MaximumProperty || + change.Property == ViewportSizeProperty || + change.Property == VisibilityProperty) { UpdateIsVisible(); } diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 1e02d70fff..e104a8a664 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -280,17 +280,13 @@ namespace Avalonia.Controls.Primitives return arrangeSize; } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 361a82e49c..78adf07e03 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -83,21 +83,17 @@ namespace Avalonia.Controls return base.ArrangeOverride(finalSize); } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == IsIndeterminateProperty) + if (change.Property == IsIndeterminateProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault(), null); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); } - else if (property == OrientationProperty) + else if (change.Property == OrientationProperty) { - UpdatePseudoClasses(null, newValue.GetValueOrDefault()); + UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 086599d0bb..24a4dc9ea6 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -375,11 +375,11 @@ namespace Avalonia.Controls _viewportManager.ResetScrollers(); } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (property == ItemsProperty) + if (change.Property == ItemsProperty) { - var newEnumerable = newValue.GetValueOrDefault(); + var newEnumerable = change.NewValue.GetValueOrDefault(); var newDataSource = newEnumerable as ItemsSourceView; if (newEnumerable != null && newDataSource == null) { @@ -388,24 +388,28 @@ namespace Avalonia.Controls OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); } - else if (property == ItemTemplateProperty) + else if (change.Property == ItemTemplateProperty) { - OnItemTemplateChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); + OnItemTemplateChanged( + change.OldValue.GetValueOrDefault(), + change.NewValue.GetValueOrDefault()); } - else if (property == LayoutProperty) + else if (change.Property == LayoutProperty) { - OnLayoutChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); + OnLayoutChanged( + change.OldValue.GetValueOrDefault(), + change.NewValue.GetValueOrDefault()); } - else if (property == HorizontalCacheLengthProperty) + else if (change.Property == HorizontalCacheLengthProperty) { - _viewportManager.HorizontalCacheLength = newValue.GetValueOrDefault(); + _viewportManager.HorizontalCacheLength = change.NewValue.GetValueOrDefault(); } - else if (property == VerticalCacheLengthProperty) + else if (change.Property == VerticalCacheLengthProperty) { - _viewportManager.VerticalCacheLength = newValue.GetValueOrDefault(); + _viewportManager.VerticalCacheLength = change.NewValue.GetValueOrDefault(); } - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); } internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index b883a76d1b..3862e87723 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -134,17 +134,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault()); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index dd00b850fe..0420356257 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -645,19 +645,15 @@ namespace Avalonia.Controls /// protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (property == SystemDecorationsProperty) + if (change.Property == SystemDecorationsProperty) { - var typedNewValue = newValue.GetValueOrDefault(); + var typedNewValue = change.NewValue.GetValueOrDefault(); PlatformImpl?.SetSystemDecorations(typedNewValue); - var o = oldValue.GetValueOrDefault() == SystemDecorations.Full; + var o = change.OldValue.GetValueOrDefault() == SystemDecorations.Full; var n = typedNewValue == SystemDecorations.Full; if (o != n) diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 20c775f965..407b28b665 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -526,17 +526,17 @@ namespace Avalonia.Input { } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == IsFocusedProperty) + if (change.Property == IsFocusedProperty) { - UpdatePseudoClasses(newValue.GetValueOrDefault(), null); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); } - else if (property == IsPointerOverProperty) + else if (change.Property == IsPointerOverProperty) { - UpdatePseudoClasses(null, newValue.GetValueOrDefault()); + UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 9b8eb4814e..4eca8df7fb 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -296,11 +296,11 @@ namespace Avalonia.Layout InvalidateLayout(); } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - var orientation = newValue.GetValueOrDefault(); + var orientation = change.NewValue.GetValueOrDefault(); //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation. diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index ee9cff4a01..84735cf66b 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -463,45 +463,44 @@ namespace Avalonia.Layout gridState.ClearElementOnDataSourceChange(context, args); } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (property == OrientationProperty) + if (change.Property == OrientationProperty) { - var orientation = newValue.GetValueOrDefault(); + var orientation = change.NewValue.GetValueOrDefault(); //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. //i.e. the properties are the inverse of each other. var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; _orientation.ScrollOrientation = scrollOrientation; } - else if (property == MinColumnSpacingProperty) + else if (change.Property == MinColumnSpacingProperty) { - _minColumnSpacing = newValue.GetValueOrDefault(); + _minColumnSpacing = change.NewValue.GetValueOrDefault(); } - else if (property == MinRowSpacingProperty) + else if (change.Property == MinRowSpacingProperty) { - _minRowSpacing = newValue.GetValueOrDefault(); + _minRowSpacing = change.NewValue.GetValueOrDefault(); } - else if (property == ItemsJustificationProperty) + else if (change.Property == ItemsJustificationProperty) { - _itemsJustification = newValue.GetValueOrDefault(); - ; + _itemsJustification = change.NewValue.GetValueOrDefault(); } - else if (property == ItemsStretchProperty) + else if (change.Property == ItemsStretchProperty) { - _itemsStretch = newValue.GetValueOrDefault(); + _itemsStretch = change.NewValue.GetValueOrDefault(); } - else if (property == MinItemWidthProperty) + else if (change.Property == MinItemWidthProperty) { - _minItemWidth = newValue.GetValueOrDefault(); + _minItemWidth = change.NewValue.GetValueOrDefault(); } - else if (property == MinItemHeightProperty) + else if (change.Property == MinItemHeightProperty) { - _minItemHeight = newValue.GetValueOrDefault(); + _minItemHeight = change.NewValue.GetValueOrDefault(); } - else if (property == MaximumRowsOrColumnsProperty) + else if (change.Property == MaximumRowsOrColumnsProperty) { - _maximumRowsOrColumns = newValue.GetValueOrDefault(); + _maximumRowsOrColumns = change.NewValue.GetValueOrDefault(); } InvalidateLayout(); diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs index 1400bc2ac3..f975862892 100644 --- a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs +++ b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs @@ -160,7 +160,7 @@ namespace Avalonia.Styling private void ConvertAndPublishNext(object? value) { - _value = value is T v ? v : BindingValue.FromUntyped(value).Convert(); + _value = value is T v ? v : BindingValue.FromUntyped(value); if (_isActive) { diff --git a/src/Avalonia.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs index 57939bab24..56c883014a 100644 --- a/src/Avalonia.Visuals/Media/DrawingImage.cs +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -63,15 +63,11 @@ namespace Avalonia.Media } /// - protected override void OnPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if (property == DrawingProperty) + if (change.Property == DrawingProperty) { RaiseInvalidated(EventArgs.Empty); } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs index 5da1f95646..6bd29a1577 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Subjects; +using Avalonia.Data; using Xunit; namespace Avalonia.Base.UnitTests @@ -63,6 +64,56 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } + [Fact] + public void GetBaseValue_LocalValue_Ignores_Default_Value() + { + var target = new Class3(); + + target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); + Assert.False(target.GetBaseValue(Class1.FooProperty, BindingPriority.LocalValue).HasValue); + } + + [Fact] + public void GetBaseValue_LocalValue_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); + } + + [Fact] + public void GetBaseValue_LocalValue_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); + } + + [Fact] + public void GetBaseValue_Style_Ignores_LocalValue_Animated_Value() + { + 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)); + } + 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 new file mode 100644 index 0000000000..e8fc3f9f40 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_OnPropertyChanged.cs @@ -0,0 +1,142 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Avalonia.Data; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_OnPropertyChanged + { + [Fact] + public void OnPropertyChangedCore_Is_Called_On_Property_Change() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + + Assert.Equal(1, target.CoreChanges.Count); + + var change = (AvaloniaPropertyChangedEventArgs)target.CoreChanges[0]; + + Assert.Equal("newvalue", change.NewValue.Value); + Assert.Equal("foodefault", change.OldValue.Value); + Assert.Equal(BindingPriority.LocalValue, change.Priority); + Assert.True(change.IsEffectiveValueChange); + } + + [Fact] + public void OnPropertyChangedCore_Is_Called_On_Non_Effective_Property_Value_Change() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style); + + Assert.Equal(2, target.CoreChanges.Count); + + var change = (AvaloniaPropertyChangedEventArgs)target.CoreChanges[1]; + + Assert.Equal("styled", change.NewValue.Value); + Assert.False(change.OldValue.HasValue); + Assert.Equal(BindingPriority.Style, change.Priority); + Assert.False(change.IsEffectiveValueChange); + } + + [Fact] + public void OnPropertyChangedCore_Is_Called_On_All_Binding_Property_Changes() + { + 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()); + } + + [Fact] + public void OnPropertyChanged_Is_Called_Only_For_Effective_Value_Changes() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, "styled", BindingPriority.Style); + + Assert.Equal(1, target.Changes.Count); + Assert.Equal(2, target.CoreChanges.Count); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", "foodefault"); + + public Class1() + { + Changes = new List(); + CoreChanges = new List(); + } + + public List Changes { get; } + public List CoreChanges { get; } + + protected override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + { + CoreChanges.Add(Clone(change)); + base.OnPropertyChangedCore(change); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + Changes.Add(Clone(change)); + base.OnPropertyChanged(change); + } + + private static AvaloniaPropertyChangedEventArgs Clone(AvaloniaPropertyChangedEventArgs change) + { + var result = new AvaloniaPropertyChangedEventArgs( + change.Sender, + change.Property, + change.OldValue, + change.NewValue, + change.Priority); + + if (!change.IsEffectiveValueChange) + { + result.MarkNonEffectiveValue(); + } + + return result; + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 941dd9bd98..3819e715f3 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Data; using Avalonia.Utilities; using Xunit; @@ -88,6 +89,19 @@ namespace Avalonia.Base.UnitTests Assert.Equal("newvalue", value); } + [Fact] + public void Changed_Observable_Fired_Only_On_Effective_Value_Change() + { + var target = new Class1(); + var result = new List(); + + Class1.FooProperty.Changed.Subscribe(x => result.Add((string)x.NewValue)); + target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); + target.SetValue(Class1.FooProperty, "local"); + + Assert.Equal(new[] { "animated" }, result); + } + [Fact] public void Property_Equals_Should_Handle_Null() { @@ -144,6 +158,11 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } + internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + { + throw new NotImplementedException(); + } + internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 5e69b8490d..0caa984a22 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -10,7 +10,7 @@ namespace Avalonia.Base.UnitTests { public class PriorityValueTests { - private static readonly IValueSink NullSink = Mock.Of(); + private static readonly IValueSink NullSink = new MockSink(); private static readonly IAvaloniaObject Owner = Mock.Of(); private static readonly StyledProperty TestProperty = new StyledProperty( "Test", @@ -30,8 +30,28 @@ namespace Avalonia.Base.UnitTests BindingPriority.StyleTrigger, NullSink)); - Assert.Equal("1", target.Value.Value); - Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority); + 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, + NullSink); + + 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] @@ -61,12 +81,31 @@ namespace Avalonia.Base.UnitTests var result = target.Entries .OfType>() - .Select(x => x.Value.Value) + .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, + NullSink); + + 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() { @@ -184,7 +223,7 @@ namespace Avalonia.Base.UnitTests target.AddBinding(source2, BindingPriority.Style).Start(); target.AddBinding(source3, BindingPriority.Style).Start(); - Assert.Equal("1", target.Value.Value); + Assert.Equal("1", target.GetValue().Value); } [Fact] @@ -196,7 +235,7 @@ namespace Avalonia.Base.UnitTests target.AddBinding(source1, BindingPriority.LocalValue).Start(); target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("2", target.Value.Value); + Assert.Equal("2", target.GetValue().Value); } [Fact] @@ -208,7 +247,7 @@ namespace Avalonia.Base.UnitTests target.AddBinding(source1, BindingPriority.Style).Start(); target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("2", target.Value.Value); + Assert.Equal("2", target.GetValue().Value); } [Fact] @@ -220,7 +259,39 @@ namespace Avalonia.Base.UnitTests target.AddBinding(source1, BindingPriority.Animation).Start(); target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("1", target.Value.Value); + Assert.Equal("1", target.GetValue().Value); + } + + [Fact] + public void NonAnimated_Value_Should_Be_Correct_1() + { + var target = new PriorityValue(Owner, TestProperty, NullSink); + 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, NullSink); + 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> @@ -239,5 +310,16 @@ namespace Avalonia.Base.UnitTests public void OnCompleted() => _observer.OnCompleted(); } + + private class MockSink : IValueSink + { + public void Completed(StyledPropertyBase property, IPriorityValueEntry entry, Optional oldValue) + { + } + + public void ValueChanged(AvaloniaPropertyChangedEventArgs change) + { + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs index 3d0cb88c8a..cb37dea220 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs @@ -18,10 +18,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml base.OnAttachedToLogicalTree(e); } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - Order.Add($"Property {property.Name} Changed"); - base.OnPropertyChanged(property, oldValue, newValue, priority); + Order.Add($"Property {change.Property.Name} Changed"); + base.OnPropertyChanged(change); } void ISupportInitialize.BeginInit() diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs index ab30d91971..52552f0bee 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "https://github.com/moq/moq4/issues/988")] public void AddDirty_With_RenderTransform_Call_RenderRoot_Invalidate() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -59,7 +59,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "https://github.com/moq/moq4/issues/988")] public void AddDirty_For_Child_Moved_Should_Invalidate_Previous_Bounds() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "https://github.com/moq/moq4/issues/988")] public void Should_Render_Child_In_Parent_With_RenderTransform() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -145,7 +145,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "https://github.com/moq/moq4/issues/988")] public void Should_Render_Child_In_Parent_With_RenderTransform2() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) From 95c04bcbec35b570684cfe13d29d898b77727a0a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 09:39:58 +0200 Subject: [PATCH 2/9] Fix bug in BindingValue. Wrong `BindingValueType` was being selected. --- src/Avalonia.Base/Data/BindingValue.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index cecdd33e7b..9aac1bacba 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -348,8 +348,8 @@ namespace Avalonia.Data return new BindingValue( fallbackValue.HasValue ? - BindingValueType.DataValidationError : - BindingValueType.DataValidationErrorWithFallback, + BindingValueType.DataValidationErrorWithFallback : + BindingValueType.DataValidationError, fallbackValue.HasValue ? fallbackValue.Value : default, e); } From c872cc005d37529017f7311d5c180708e73ad9b2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 15:15:31 +0200 Subject: [PATCH 3/9] Only call property notify on effective value change. --- src/Avalonia.Base/AvaloniaObject.cs | 10 ++++++++-- .../AvaloniaPropertyTests.cs | 20 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 546ab78e31..f387d7e0b6 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -716,7 +716,10 @@ namespace Avalonia { VerifyAccess(); - change.Property.Notifying?.Invoke(this, true); + if (change.IsEffectiveValueChange) + { + change.Property.Notifying?.Invoke(this, true); + } try { @@ -747,7 +750,10 @@ namespace Avalonia } finally { - change.Property.Notifying?.Invoke(this, false); + if (change.IsEffectiveValueChange) + { + change.Property.Notifying?.Invoke(this, false); + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 3819e715f3..d7f927372e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -102,6 +102,17 @@ namespace Avalonia.Base.UnitTests Assert.Equal(new[] { "animated" }, result); } + [Fact] + public void Notify_Fired_Only_On_Effective_Value_Change() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); + target.SetValue(Class1.FooProperty, "local"); + + Assert.Equal(2, target.NotifyCount); + } + [Fact] public void Property_Equals_Should_Handle_Null() { @@ -180,7 +191,14 @@ namespace Avalonia.Base.UnitTests private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = - AvaloniaProperty.Register("Foo", "default"); + AvaloniaProperty.Register("Foo", "default", notifying: FooNotifying); + + public int NotifyCount { get; private set; } + + private static void FooNotifying(IAvaloniaObject o, bool n) + { + ++((Class1)o).NotifyCount; + } } private class Class2 : Class1 From d1fc5ee92e7028b5cfa626de79072d74abe1b176 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Apr 2020 17:02:07 +0200 Subject: [PATCH 4/9] Reworked transitions. - Don't apply transitions before control is added to the visual tree - Don't allow transitions on direct properties - Ignore animated values when determining whether to trigger a transition - Cancel transition when removed from `Transitions` collection --- src/Avalonia.Animation/Animatable.cs | 215 +++++++++++++--- src/Avalonia.Animation/Transitions.cs | 13 + src/Avalonia.Visuals/Visual.cs | 2 + .../AnimatableTests.cs | 242 ++++++++++++++++++ 4 files changed, 441 insertions(+), 31 deletions(-) create mode 100644 tests/Avalonia.Animation.UnitTests/AnimatableTests.cs diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 324ff06452..eb94546969 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -1,10 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Collections; +using System.Collections.Specialized; using Avalonia.Data; -using Avalonia.Animation.Animators; + +#nullable enable namespace Avalonia.Animation { @@ -13,15 +13,12 @@ namespace Avalonia.Animation /// public class Animatable : AvaloniaObject { + /// + /// Defines the property. + /// public static readonly StyledProperty ClockProperty = AvaloniaProperty.Register(nameof(Clock), inherits: true); - public IClock Clock - { - get => GetValue(ClockProperty); - set => SetValue(ClockProperty, value); - } - /// /// Defines the property. /// @@ -31,9 +28,18 @@ namespace Avalonia.Animation o => o.Transitions, (o, v) => o.Transitions = v); - private Transitions _transitions; + private bool _transitionsEnabled; + private Transitions? _transitions; + private Dictionary? _transitionState; - private Dictionary _previousTransitions; + /// + /// Gets or sets the clock which controls the animations on the control. + /// + public IClock Clock + { + get => GetValue(ClockProperty); + set => SetValue(ClockProperty, value); + } /// /// Gets or sets the property transitions for the control. @@ -43,48 +49,195 @@ namespace Avalonia.Animation get { if (_transitions is null) + { _transitions = new Transitions(); - - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); + _transitions.CollectionChanged += TransitionsCollectionChanged; + } return _transitions; } set { + // TODO: This is a hack, Setter should not replace transitions, but should add/remove. if (value is null) + { return; + } - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); + if (_transitions is object) + { + RemoveTransitions(_transitions); + _transitions.CollectionChanged -= TransitionsCollectionChanged; + } SetAndRaise(TransitionsProperty, ref _transitions, value); + _transitions.CollectionChanged += TransitionsCollectionChanged; + AddTransitions(_transitions); + } + } + + /// + /// Enables transitions for the control. + /// + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void EnableTransitions() + { + if (!_transitionsEnabled) + { + _transitionsEnabled = true; + + if (_transitions is object) + { + AddTransitions(_transitions); + } + } + } + + /// + /// Disables transitions for the control. + /// + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void DisableTransitions() + { + if (_transitionsEnabled) + { + _transitionsEnabled = false; + + if (_transitions is object) + { + RemoveTransitions(_transitions); + } + } + } + + protected override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + { + if (_transitionsEnabled && + _transitions is object && + _transitionState is object && + change.Priority > BindingPriority.Animation) + { + foreach (var transition in _transitions) + { + if (transition.Property == change.Property) + { + var state = _transitionState[transition]; + var oldValue = state.BaseValue; + var newValue = GetAnimationBaseValue(transition.Property); + + if (!Equals(oldValue, newValue)) + { + state.BaseValue = newValue; + + // We need to transition from the current animated value if present, + // instead of the old base value. + var animatedValue = GetValue(transition.Property); + + if (!Equals(newValue, animatedValue)) + { + oldValue = animatedValue; + } + + state.Instance?.Dispose(); + state.Instance = transition.Apply( + this, + Clock ?? AvaloniaLocator.Current.GetService(), + oldValue, + newValue); + return; + } + } + } + } + + base.OnPropertyChangedCore(change); + } + + private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (!_transitionsEnabled) + { + return; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + AddTransitions(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + RemoveTransitions(e.OldItems); + break; + case NotifyCollectionChangedAction.Replace: + RemoveTransitions(e.OldItems); + AddTransitions(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException("Transitions collection cannot be reset."); } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + private void AddTransitions(IList items) { - if (_transitions is null || _previousTransitions is null || change.Priority == BindingPriority.Animation) + if (!_transitionsEnabled) + { return; + } - // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). - foreach (var transition in _transitions) + _transitionState ??= new Dictionary(); + + for (var i = 0; i < items.Count; ++i) { - if (transition.Property == change.Property) + var t = (ITransition)items[i]; + + _transitionState.Add(t, new TransitionState { - if (_previousTransitions.TryGetValue(change.Property, out var dispose)) - dispose.Dispose(); + BaseValue = GetAnimationBaseValue(t.Property), + }); + } + } - var instance = transition.Apply( - this, - Clock ?? Avalonia.Animation.Clock.GlobalClock, - change.OldValue.GetValueOrDefault(), - change.NewValue.GetValueOrDefault()); + private void RemoveTransitions(IList items) + { + if (_transitionState is null) + { + return; + } - _previousTransitions[change.Property] = instance; - return; + for (var i = 0; i < items.Count; ++i) + { + var t = (ITransition)items[i]; + + if (_transitionState.TryGetValue(t, out var state)) + { + state.Instance?.Dispose(); + _transitionState.Remove(t); } } } + + private object GetAnimationBaseValue(AvaloniaProperty property) + { + var value = this.GetBaseValue(property, BindingPriority.LocalValue); + + if (value == AvaloniaProperty.UnsetValue) + { + value = GetValue(property); + } + + return value; + } + + private class TransitionState + { + public IDisposable? Instance { get; set; } + public object? BaseValue { get; set; } + } } } diff --git a/src/Avalonia.Animation/Transitions.cs b/src/Avalonia.Animation/Transitions.cs index 2741039ebc..6687a2902d 100644 --- a/src/Avalonia.Animation/Transitions.cs +++ b/src/Avalonia.Animation/Transitions.cs @@ -1,4 +1,6 @@ +using System; using Avalonia.Collections; +using Avalonia.Threading; namespace Avalonia.Animation { @@ -13,6 +15,17 @@ namespace Avalonia.Animation public Transitions() { ResetBehavior = ResetBehavior.Remove; + Validate = ValidateTransition; + } + + private void ValidateTransition(ITransition obj) + { + Dispatcher.UIThread.VerifyAccess(); + + if (obj.Property.IsDirect) + { + throw new InvalidOperationException("Cannot animate a direct property."); + } } } } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 36b72fa28e..4d96fff3f6 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -393,6 +393,7 @@ namespace Avalonia RenderTransform.Changed += RenderTransformChanged; } + EnableTransitions(); OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); @@ -429,6 +430,7 @@ namespace Avalonia RenderTransform.Changed -= RenderTransformChanged; } + DisableTransitions(); OnDetachedFromVisualTree(e); DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs new file mode 100644 index 0000000000..e1169650a9 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -0,0 +1,242 @@ +using System; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Animation.UnitTests +{ + public class AnimatableTests + { + [Fact] + public void Transition_Is_Not_Applied_When_Not_Attached_To_Visual_Tree() + { + var target = CreateTarget(); + var control = new Control + { + Transitions = { target.Object }, + }; + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5), + Times.Never); + } + + [Fact] + public void Transition_Is_Not_Applied_To_Initial_Style() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = CreateTarget(); + var control = new Control + { + Transitions = { target.Object }, + }; + + var root = new TestRoot + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Visual.OpacityProperty, 0.8), + } + } + } + }; + + root.Child = control; + + Assert.Equal(0.8, control.Opacity); + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + } + + [Fact] + public void Transition_Is_Applied_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)); + } + + [Fact] + public void Transition_Is_Not_Applied_When_Animated_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.SetValue(Visual.OpacityProperty, 0.5, BindingPriority.Animation); + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5), + Times.Never); + } + + [Fact] + public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.SetValue(Visual.OpacityProperty, 0.5); + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)); + target.ResetCalls(); + + control.SetValue(Visual.OpacityProperty, 0.8, BindingPriority.StyleTrigger); + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public void Transition_Is_Disposed_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)).Returns(sub.Object); + + control.Opacity = 0.5; + sub.ResetCalls(); + control.Opacity = 0.4; + + sub.Verify(x => x.Dispose()); + } + + [Fact] + public void New_Transition_Is_Applied_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + target.Setup(x => x.Property).Returns(Visual.OpacityProperty); + target.Setup(x => x.Apply(control, It.IsAny(), 1.0, 0.5)) + .Callback(() => + { + control.SetValue(Visual.OpacityProperty, 0.9, BindingPriority.Animation); + }) + .Returns(Mock.Of()); + + control.Opacity = 0.5; + + Assert.Equal(0.9, control.Opacity); + target.ResetCalls(); + + control.Opacity = 0.4; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 0.9, + 0.4)); + } + + [Fact] + public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)); + target.ResetCalls(); + + var root = (TestRoot)control.Parent; + root.Child = null; + control.Opacity = 0.8; + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public void Animation_Is_Cancelled_When_Transition_Removed() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(sub.Object); + + control.Opacity = 0.5; + control.Transitions.RemoveAt(0); + + sub.Verify(x => x.Dispose()); + } + + private static Mock CreateTarget() + { + var target = new Mock(); + var sub = new Mock(); + + target.Setup(x => x.Property).Returns(Visual.OpacityProperty); + target.Setup(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(sub.Object); + + return target; + } + + private static Control CreateControl(ITransition transition) + { + var control = new Control + { + Transitions = { transition }, + }; + + var root = new TestRoot(control); + return control; + } + } +} From 1151c929ca3b8a4469fd9fa9d0327d6a1d647b2f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 14:46:38 +0200 Subject: [PATCH 5/9] Seal OnPropertyChangedCore for now. It's only likely to be needed by transitions, and overriding it will break transitions. We can unseal later if there's a user need for it. --- src/Avalonia.Animation/Animatable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index eb94546969..31670454c9 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -116,7 +116,7 @@ namespace Avalonia.Animation } } - protected override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) { if (_transitionsEnabled && _transitions is object && From 83098e99233abd9f34f36e1fa33c68d294e9f791 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 15:29:24 +0200 Subject: [PATCH 6/9] Enable transitions for non-Visuals. `Visual` knows when it's attached to the visual tree, so it can enable transitions then. This is a hack due to the fact that non-Visuals currently don't know when to enable transitions. --- src/Avalonia.Animation/Animatable.cs | 2 +- src/Avalonia.Visuals/Visual.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 31670454c9..7d6df716b8 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -28,7 +28,7 @@ namespace Avalonia.Animation o => o.Transitions, (o, v) => o.Transitions = v); - private bool _transitionsEnabled; + private bool _transitionsEnabled = true; private Transitions? _transitions; private Dictionary? _transitionState; diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 4d96fff3f6..bb9a4cf208 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -114,6 +114,9 @@ namespace Avalonia /// public Visual() { + // Disable transitions until we're added to the visual tree. + DisableTransitions(); + var visualChildren = new AvaloniaList(); visualChildren.ResetBehavior = ResetBehavior.Remove; visualChildren.Validate = visual => ValidateVisualChild(visual); From 8e7c0e1c59e1d313c6a72637112cf03267aeac96 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 6 May 2020 16:29:28 +0200 Subject: [PATCH 7/9] Make Transitions a styled property. This requires that a `` element is used to contain the transitions in XAML and a `new Transitions()` is used when assigning transitions from code. --- src/Avalonia.Animation/Animatable.cs | 76 +++++------ src/Avalonia.Animation/TransitionInstance.cs | 2 + .../AnimatableTests.cs | 123 ++++++++++++++++-- .../TransitionsTests.cs | 4 +- .../Xaml/StyleTests.cs | 45 +++++++ .../Xaml/XamlIlTests.cs | 8 +- 6 files changed, 196 insertions(+), 62 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 7d6df716b8..9e9b84537b 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -22,14 +22,10 @@ namespace Avalonia.Animation /// /// Defines the property. /// - public static readonly DirectProperty TransitionsProperty = - AvaloniaProperty.RegisterDirect( - nameof(Transitions), - o => o.Transitions, - (o, v) => o.Transitions = v); + public static readonly StyledProperty TransitionsProperty = + AvaloniaProperty.Register(nameof(Transitions)); private bool _transitionsEnabled = true; - private Transitions? _transitions; private Dictionary? _transitionState; /// @@ -44,36 +40,10 @@ namespace Avalonia.Animation /// /// Gets or sets the property transitions for the control. /// - public Transitions Transitions + public Transitions? Transitions { - get - { - if (_transitions is null) - { - _transitions = new Transitions(); - _transitions.CollectionChanged += TransitionsCollectionChanged; - } - - return _transitions; - } - set - { - // TODO: This is a hack, Setter should not replace transitions, but should add/remove. - if (value is null) - { - return; - } - - if (_transitions is object) - { - RemoveTransitions(_transitions); - _transitions.CollectionChanged -= TransitionsCollectionChanged; - } - - SetAndRaise(TransitionsProperty, ref _transitions, value); - _transitions.CollectionChanged += TransitionsCollectionChanged; - AddTransitions(_transitions); - } + get => GetValue(TransitionsProperty); + set => SetValue(TransitionsProperty, value); } /// @@ -89,9 +59,9 @@ namespace Avalonia.Animation { _transitionsEnabled = true; - if (_transitions is object) + if (Transitions is object) { - AddTransitions(_transitions); + AddTransitions(Transitions); } } } @@ -109,21 +79,39 @@ namespace Avalonia.Animation { _transitionsEnabled = false; - if (_transitions is object) + if (Transitions is object) { - RemoveTransitions(_transitions); + RemoveTransitions(Transitions); } } } protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) { - if (_transitionsEnabled && - _transitions is object && - _transitionState is object && - change.Priority > BindingPriority.Animation) + if (change.Property == TransitionsProperty && change.IsEffectiveValueChange) + { + var oldTransitions = change.OldValue.GetValueOrDefault(); + var newTransitions = change.NewValue.GetValueOrDefault(); + + if (oldTransitions is object) + { + oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + RemoveTransitions(oldTransitions); + } + + if (newTransitions is object) + { + newTransitions.CollectionChanged += TransitionsCollectionChanged; + AddTransitions(newTransitions); + } + } + else if (_transitionsEnabled && + Transitions is object && + _transitionState is object && + !change.Property.IsDirect && + change.Priority > BindingPriority.Animation) { - foreach (var transition in _transitions) + foreach (var transition in Transitions) { if (transition.Property == change.Property) { diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index efbbed51b5..ad2001d621 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -19,6 +19,8 @@ namespace Avalonia.Animation public TransitionInstance(IClock clock, TimeSpan Duration) { + clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _duration = Duration; _baseClock = clock; } diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs index e1169650a9..b5c61883e7 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Layout; using Avalonia.Styling; using Avalonia.UnitTests; using Moq; @@ -16,7 +17,7 @@ namespace Avalonia.Animation.UnitTests var target = CreateTarget(); var control = new Control { - Transitions = { target.Object }, + Transitions = new Transitions { target.Object }, }; control.Opacity = 0.5; @@ -37,7 +38,7 @@ namespace Avalonia.Animation.UnitTests var target = CreateTarget(); var control = new Control { - Transitions = { target.Object }, + Transitions = new Transitions { target.Object }, }; var root = new TestRoot @@ -213,30 +214,126 @@ namespace Avalonia.Animation.UnitTests sub.Verify(x => x.Dispose()); } - private static Mock CreateTarget() + [Fact] + public void Animation_Is_Cancelled_When_New_Style_Activates() { - var target = new Mock(); - var sub = new Mock(); + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = CreateTarget(); + var control = CreateStyledControl(target.Object); + var sub = new Mock(); - target.Setup(x => x.Property).Returns(Visual.OpacityProperty); - target.Setup(x => x.Apply( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())).Returns(sub.Object); + target.Setup(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)).Returns(sub.Object); - return target; + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5), + Times.Once); + + control.Classes.Add("foo"); + + sub.Verify(x => x.Dispose()); + } + } + + [Fact] + public void Transition_From_Style_Trigger_Is_Applied() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = CreateTransition(Control.WidthProperty); + var control = CreateStyledControl(transition2: target.Object); + var sub = new Mock(); + + control.Classes.Add("foo"); + control.Width = 100; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + double.NaN, + 100.0), + Times.Once); + } + } + + private static Mock CreateTarget() + { + return CreateTransition(Visual.OpacityProperty); } private static Control CreateControl(ITransition transition) { var control = new Control { - Transitions = { transition }, + Transitions = new Transitions { transition }, }; var root = new TestRoot(control); return control; } + + private static Control CreateStyledControl( + ITransition transition1 = null, + ITransition transition2 = null) + { + transition1 = transition1 ?? CreateTarget().Object; + transition2 = transition2 ?? CreateTransition(Control.WidthProperty).Object; + + var control = new Control + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Control.TransitionsProperty, + Value = new Transitions { transition1 }, + } + } + }, + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter + { + Property = Control.TransitionsProperty, + Value = new Transitions { transition2 }, + } + } + } + } + }; + + var root = new TestRoot(control); + return control; + } + + private static Mock CreateTransition(AvaloniaProperty property) + { + var target = new Mock(); + var sub = new Mock(); + + target.Setup(x => x.Property).Returns(property); + target.Setup(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(sub.Object); + + return target; + } } } diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index 22f3b4f501..70ffd781a1 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -16,7 +16,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { @@ -44,7 +44,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 02f0d7072c..9642f5719d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -337,5 +337,50 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, listBox.Background); } } + + [Fact] + public void Transitions_Can_Be_Styled() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var border = (Border)window.Content; + + Assert.Equal(1, border.Transitions.Count); + Assert.Equal(Border.WidthProperty, border.Transitions[0].Property); + + border.Classes.Add("foo"); + + Assert.Equal(1, border.Transitions.Count); + Assert.Equal(Border.HeightProperty, border.Transitions[0].Property); + + border.Classes.Remove("foo"); + + Assert.Equal(1, border.Transitions.Count); + Assert.Equal(Border.WidthProperty, border.Transitions[0].Property); + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 4ff9e3db38..a408069cb0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -34,9 +34,11 @@ namespace Avalonia.Markup.Xaml.UnitTests var parsed = (Grid)AvaloniaXamlLoader.Parse(@" - + + + "); Assert.Equal(1, parsed.Transitions.Count); From 7962cc4ceb9b5e025df506223180ab4b68a4c2f3 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 12 May 2020 17:08:24 +0200 Subject: [PATCH 8/9] Don't invalidate measure on invalidate formatted text --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 9cbde72f7f..09f86f462c 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -75,6 +75,9 @@ namespace Avalonia.Controls.Presenters static TextPresenter() { AffectsRender(SelectionBrushProperty); + AffectsMeasure(TextProperty, PasswordCharProperty, + TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, + TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed, @@ -284,8 +287,6 @@ namespace Avalonia.Controls.Presenters protected void InvalidateFormattedText() { _formattedText = null; - - InvalidateMeasure(); } /// @@ -301,13 +302,15 @@ namespace Avalonia.Controls.Presenters context.FillRectangle(background, new Rect(Bounds.Size)); } - FormattedText.Constraint = Bounds.Size; - context.DrawText(Foreground, new Point(), FormattedText); } public override void Render(DrawingContext context) { + FormattedText.Constraint = Bounds.Size; + + _constraint = Bounds.Size; + var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -316,10 +319,6 @@ namespace Avalonia.Controls.Presenters var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; - // issue #600: set constraint before any FormattedText manipulation - // see base.Render(...) implementation - FormattedText.Constraint = _constraint; - var rects = FormattedText.HitTestTextRange(start, length); foreach (var rect in rects) From 234664011c88711610326751c4b5823ff439d1f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 16 May 2020 16:21:12 +0200 Subject: [PATCH 9/9] Fixed typo. --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 896f922fcd..cfe47a09d5 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -1,5 +1,4 @@ -// (c) Copyright Microsoft Corporation. -// This source is subject to the Microsoft Public License (Ms-PL). +// This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. @@ -767,7 +766,7 @@ namespace Avalonia.Controls /// /// ItemsProperty property changed handler. /// - /// AvaloniaPropertyChangedEventArgsdEventArgs. + /// The event arguments. private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (!_areHandlersSuspended)