From 4a752c3f48f355a442694574fa1e068aa0cfdf10 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 09:39:22 +0200 Subject: [PATCH 1/4] 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/4] 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/4] 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 234664011c88711610326751c4b5823ff439d1f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 16 May 2020 16:21:12 +0200 Subject: [PATCH 4/4] 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)