From 24f208bab1fce4a7ed9403703a0b7f9ff07d2c2b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 25 Mar 2019 17:45:42 +0100 Subject: [PATCH 1/4] !R Rework deferred setter for better performance. --- src/Avalonia.Base/AvaloniaObject.cs | 91 +++++++++-- .../Utilities/AvaloniaPropertyCollection.cs | 143 ++++++++++++++++ .../Utilities/DeferredSetterOptimized.cs | 84 ++++++++++ src/Avalonia.Base/ValueStore.cs | 153 ++++-------------- .../Base/DirectPropertyBenchmark.cs | 82 ++++++++++ tests/Avalonia.Benchmarks/Base/Properties.cs | 3 +- 6 files changed, 420 insertions(+), 136 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs create mode 100644 src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs create mode 100644 tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7601b64ce9..22aeccc3fb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -561,6 +561,7 @@ namespace Avalonia protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) { VerifyAccess(); + return SetAndRaise( property, ref field, @@ -568,19 +569,89 @@ namespace Avalonia => SetAndRaiseCore(property, ref backing, val, notifyWrapper), value); } + + /// + /// Default setter handler that will set backing field and raise notification. + /// + private sealed class DefaultSetterHandler : DeferredSetterOptimized.ISetterHandler + { + public static readonly DefaultSetterHandler Instance = new DefaultSetterHandler(); + + public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) + { + var old = backing; + backing = value; + + source.RaisePropertyChanged(property, old, value); + + return true; + } + } /// - /// Default assignment logic for SetAndRaise. + /// Setter handler that will run custom user callback. /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) + private sealed class CallbackSetterHandler : DeferredSetterOptimized.ISetterHandler + { + private readonly SetAndRaiseCallback _callback; + + public CallbackSetterHandler(SetAndRaiseCallback callback) + { + _callback = callback; + } + + public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) + { + _callback(value, ref backing, notification => notification()); + + return true; + } + } + + protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) + { + VerifyAccess(); + + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + + return setter.SetAndNotify(this, property, DefaultSetterHandler.Instance, ref field, value); + } + + protected bool SetAndRaiseOptimized( + AvaloniaProperty property, + ref T field, + SetAndRaiseCallback setterCallback, + T value) + { + VerifyAccess(); + + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + + return setter.SetAndNotify(this, property, new CallbackSetterHandler(setterCallback) , ref field, value); + } + + /// + /// Default assignment logic for SetAndRaise. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// A wrapper for the property-changed notification. + /// + /// True if the value changed, otherwise false. + /// + private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) { var old = field; field = value; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs new file mode 100644 index 0000000000..5b1492a7a8 --- /dev/null +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + internal sealed class AvaloniaPropertyCollection + { + private Entry[] _entries; + + public AvaloniaPropertyCollection() + { + // The last item in the list is always int.MaxValue + _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; + } + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 12) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) + return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) + return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) + return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) + return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) + return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) + return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) + return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) + return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) + return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) + return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) + return (10, _entries[10].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + + return (0, false); + } + + public bool TryGetValue(AvaloniaProperty property, out TValue value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = default; + return false; + } + + value = _entries[index].Value; + return true; + } + + public void AddValueInternal(AvaloniaProperty property, TValue value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + public void SetValueInternal(AvaloniaProperty property, TValue value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + public Dictionary ToDictionary() + { + var dict = new Dictionary(_entries.Length - 1); + + for (int i = 0; i < _entries.Length - 1; ++i) + { + dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); + } + + return dict; + } + + private struct Entry + { + internal int PropertyId; + internal TValue Value; + } + } +} diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs new file mode 100644 index 0000000000..e3bed90498 --- /dev/null +++ b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs @@ -0,0 +1,84 @@ +using System; + +namespace Avalonia.Utilities +{ + /// + /// A utility class to enable deferring assignment until after property-changed notifications are sent. + /// Used to fix #855. + /// + /// The type of value with which to track the delayed assignment. + internal sealed class DeferredSetterOptimized + { + private bool _isNotifying; + private readonly SingleOrQueue _pendingValues; + + public DeferredSetterOptimized() + { + _pendingValues = new SingleOrQueue(); + } + + public bool SetAndNotify( + AvaloniaObject source, + AvaloniaProperty property, + ISetterHandler handler, + ref TSetRecord backing, + TSetRecord value) + { + if (!_isNotifying) + { + bool updated; + + using (new NotifyDisposable(this)) + { + updated = handler.Update(source, property, ref backing, value); + } + + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + updated = handler.Update(source, property, ref backing, _pendingValues.Dequeue()); + } + } + } + + return updated; + } + + _pendingValues.Enqueue(value); + + return false; + } + + /// + /// Disposable that marks the property as currently notifying. + /// When disposed, marks the property as done notifying. + /// + private readonly struct NotifyDisposable : IDisposable + { + private readonly DeferredSetterOptimized _setter; + + internal NotifyDisposable(DeferredSetterOptimized setter) + { + _setter = setter; + _setter._isNotifying = true; + } + + public void Dispose() + { + _setter._isNotifying = false; + } + } + + public interface ISetterHandler + { + bool Update( + AvaloniaObject source, + AvaloniaProperty property, + ref TSetRecord backing, + TSetRecord value); + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 24f85ea6b1..b2c4c68f9c 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,21 +7,15 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - private struct Entry - { - internal int PropertyId; - internal object Value; - } - + private readonly AvaloniaPropertyCollection _propertyValues; + private readonly AvaloniaPropertyCollection _deferredSetters; private readonly AvaloniaObject _owner; - private Entry[] _entries; public ValueStore(AvaloniaObject owner) { _owner = owner; - - // The last item in the list is always int.MaxValue - _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } }; + _propertyValues = new AvaloniaPropertyCollection(); + _deferredSetters = new AvaloniaPropertyCollection(); } public IDisposable AddBinding( @@ -31,7 +25,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -39,13 +33,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValueInternal(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValueInternal(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -55,7 +49,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -63,7 +57,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - SetValueInternal(property, Validate(property, value)); + _propertyValues.SetValueInternal(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -71,7 +65,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValueInternal(property, priorityValue); } } } @@ -84,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValueInternal(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValueInternal(property, priorityValue); } } @@ -110,14 +104,9 @@ namespace Avalonia public IDictionary GetSetValues() { - var dict = new Dictionary(_entries.Length - 1); - for (int i = 0; i < _entries.Length - 1; ++i) - { - dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); - } - - return dict; + return _propertyValues.ToDictionary(); } + public void LogError(AvaloniaProperty property, Exception e) { _owner.LogBindingError(property, e); @@ -127,7 +116,7 @@ namespace Avalonia { var result = AvaloniaProperty.UnsetValue; - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; } @@ -137,12 +126,12 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; } @@ -152,7 +141,7 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { (value as PriorityValue)?.Revalidate(); } @@ -189,113 +178,29 @@ namespace Avalonia return value; } - private DeferredSetter _deferredSetter; - - public DeferredSetter Setter - { - get - { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); - } - } - - private bool TryGetValue(AvaloniaProperty property, out object value) + public DeferredSetterOptimized GetDeferredSetter(AvaloniaProperty property) { - (int index, bool found) = TryFindEntry(property.Id); - if (!found) + if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - value = null; - return false; + return (DeferredSetterOptimized)deferredSetter; } - value = _entries[index].Value; - return true; - } - - private void AddValueInternal(AvaloniaProperty property, object value) - { - Entry[] entries = new Entry[_entries.Length + 1]; - - for (int i = 0; i < _entries.Length; ++i) - { - if (_entries[i].PropertyId > property.Id) - { - if (i > 0) - { - Array.Copy(_entries, 0, entries, 0, i); - } + var newDeferredSetter = new DeferredSetterOptimized(); - entries[i] = new Entry { PropertyId = property.Id, Value = value }; - Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); - break; - } - } + _deferredSetters.AddValueInternal(property, newDeferredSetter); - _entries = entries; + return newDeferredSetter; } - private void SetValueInternal(AvaloniaProperty property, object value) - { - _entries[TryFindEntry(property.Id).Item1].Value = value; - } + private DeferredSetter _deferredSetter; - private (int, bool) TryFindEntry(int propertyId) + public DeferredSetter Setter { - if (_entries.Length <= 12) - { - // For small lists, we use an optimized linear search. Since the last item in the list - // is always int.MaxValue, we can skip a conditional branch in each iteration. - // By unrolling the loop, we can skip another unconditional branch in each iteration. - - if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId); - if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId); - if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId); - if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId); - if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId); - if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId); - if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId); - if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId); - if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); - if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); - if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); - } - else + get { - int low = 0; - int high = _entries.Length; - int id; - - while (high - low > 3) - { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; - - if (propertyId == id) - return (pivot, true); - - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; - } - - do - { - id = _entries[low].PropertyId; - - if (id == propertyId) - return (low, true); - - if (id > propertyId) - break; - - ++low; - } - while (low < high); + return _deferredSetter ?? + (_deferredSetter = new DeferredSetter()); } - - return (0, false); } } } diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs new file mode 100644 index 0000000000..4f36b54414 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -0,0 +1,82 @@ +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class DirectPropertyBenchmark + { + [Benchmark(Baseline = true)] + public void SetAndRaiseOriginal() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValue += 1; + } + } + + [Benchmark] + public void SetAndRaiseOptimized() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValueOptimized += 1; + } + } + + [Benchmark] + public void SetAndRaiseSimple() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValueSimple += 1; + } + } + + class DirectClass : AvaloniaObject + { + private int _intValue; + + public static readonly DirectProperty IntValueProperty = + AvaloniaProperty.RegisterDirect(nameof(IntValue), + o => o.IntValue, + (o, v) => o.IntValue = v); + + public int IntValue + { + get => _intValue; + set => SetAndRaise(IntValueProperty, ref _intValue, value); + } + + public int IntValueOptimized + { + get => _intValue; + set => SetAndRaiseOptimized(IntValueProperty, ref _intValue, value); + } + + public int IntValueSimple + { + get => _intValue; + set + { + VerifyAccess(); + + if (_intValue == value) + { + return; + } + + var old = _intValue; + _intValue = value; + + RaisePropertyChanged(IntValueProperty, old, _intValue); + } + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Benchmarks/Base/Properties.cs b/tests/Avalonia.Benchmarks/Base/Properties.cs index 0a020961d5..45fc68ac96 100644 --- a/tests/Avalonia.Benchmarks/Base/Properties.cs +++ b/tests/Avalonia.Benchmarks/Base/Properties.cs @@ -1,5 +1,4 @@ -using System; -using System.Reactive.Subjects; +using System.Reactive.Subjects; using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Base From 812c5f30c90d09473f40375ebe59e42641843608 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 21 Jul 2019 17:23:11 +0200 Subject: [PATCH 2/4] Remove SetAndRaise callback handling. Move event raising to setter. --- src/Avalonia.Base/AvaloniaObject.cs | 82 +++---------------- .../Utilities/AvaloniaPropertyCollection.cs | 4 +- .../Utilities/DeferredSetterOptimized.cs | 27 +++--- src/Avalonia.Base/ValueStore.cs | 14 ++-- .../Avalonia.Benchmarks.csproj | 2 +- .../Base/DirectPropertyBenchmark.cs | 2 +- 6 files changed, 36 insertions(+), 95 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 22aeccc3fb..99e27e7daf 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -466,7 +466,7 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected void RaisePropertyChanged( + protected internal void RaisePropertyChanged( AvaloniaProperty property, object oldValue, object newValue, @@ -569,44 +569,6 @@ namespace Avalonia => SetAndRaiseCore(property, ref backing, val, notifyWrapper), value); } - - /// - /// Default setter handler that will set backing field and raise notification. - /// - private sealed class DefaultSetterHandler : DeferredSetterOptimized.ISetterHandler - { - public static readonly DefaultSetterHandler Instance = new DefaultSetterHandler(); - - public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) - { - var old = backing; - backing = value; - - source.RaisePropertyChanged(property, old, value); - - return true; - } - } - - /// - /// Setter handler that will run custom user callback. - /// - private sealed class CallbackSetterHandler : DeferredSetterOptimized.ISetterHandler - { - private readonly SetAndRaiseCallback _callback; - - public CallbackSetterHandler(SetAndRaiseCallback callback) - { - _callback = callback; - } - - public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) - { - _callback(value, ref backing, notification => notification()); - - return true; - } - } protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) { @@ -619,39 +581,21 @@ namespace Avalonia DeferredSetterOptimized setter = Values.GetDeferredSetter(property); - return setter.SetAndNotify(this, property, DefaultSetterHandler.Instance, ref field, value); - } - - protected bool SetAndRaiseOptimized( - AvaloniaProperty property, - ref T field, - SetAndRaiseCallback setterCallback, - T value) - { - VerifyAccess(); - - if (EqualityComparer.Default.Equals(field, value)) - { - return false; - } - - DeferredSetterOptimized setter = Values.GetDeferredSetter(property); - - return setter.SetAndNotify(this, property, new CallbackSetterHandler(setterCallback) , ref field, value); + return setter.SetAndNotify(this, property, ref field, value); } /// - /// Default assignment logic for SetAndRaise. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) + /// Default assignment logic for SetAndRaise. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// A wrapper for the property-changed notification. + /// + /// True if the value changed, otherwise false. + /// + private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) { var old = field; field = value; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs index 5b1492a7a8..9b64342814 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs @@ -95,7 +95,7 @@ namespace Avalonia.Utilities return true; } - public void AddValueInternal(AvaloniaProperty property, TValue value) + public void AddValue(AvaloniaProperty property, TValue value) { Entry[] entries = new Entry[_entries.Length + 1]; @@ -117,7 +117,7 @@ namespace Avalonia.Utilities _entries = entries; } - public void SetValueInternal(AvaloniaProperty property, TValue value) + public void SetValue(AvaloniaProperty property, TValue value) { _entries[TryFindEntry(property.Id).Item1].Value = value; } diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs index e3bed90498..4858372828 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs @@ -17,20 +17,26 @@ namespace Avalonia.Utilities _pendingValues = new SingleOrQueue(); } + private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) + { + var old = backing; + + backing = value; + + source.RaisePropertyChanged(property, old, value); + } + public bool SetAndNotify( AvaloniaObject source, AvaloniaProperty property, - ISetterHandler handler, ref TSetRecord backing, TSetRecord value) { if (!_isNotifying) { - bool updated; - using (new NotifyDisposable(this)) { - updated = handler.Update(source, property, ref backing, value); + SetAndRaisePropertyChanged(source, property, ref backing, value); } if (!_pendingValues.Empty) @@ -39,12 +45,12 @@ namespace Avalonia.Utilities { while (!_pendingValues.Empty) { - updated = handler.Update(source, property, ref backing, _pendingValues.Dequeue()); + SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); } } } - return updated; + return true; } _pendingValues.Enqueue(value); @@ -71,14 +77,5 @@ namespace Avalonia.Utilities _setter._isNotifying = false; } } - - public interface ISetterHandler - { - bool Update( - AvaloniaObject source, - AvaloniaProperty property, - ref TSetRecord backing, - TSetRecord value); - } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index b2c4c68f9c..b7f5c26801 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -33,13 +33,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - _propertyValues.AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -57,7 +57,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.SetValueInternal(property, Validate(property, value)); + _propertyValues.SetValue(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -65,7 +65,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } } @@ -78,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValue(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - _propertyValues.AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } } @@ -187,7 +187,7 @@ namespace Avalonia var newDeferredSetter = new DeferredSetterOptimized(); - _deferredSetters.AddValueInternal(property, newDeferredSetter); + _deferredSetters.AddValue(property, newDeferredSetter); return newDeferredSetter; } diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 6550a23b7b..f503bf66a7 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -18,7 +18,7 @@ - + diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs index 4f36b54414..ecf8a1fb2b 100644 --- a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -79,4 +79,4 @@ namespace Avalonia.Benchmarks.Base } } } -} \ No newline at end of file +} From 48be9dc2600acf8a3269e6f93206b6b7d4f275cd Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 21 Jul 2019 22:37:19 +0200 Subject: [PATCH 3/4] Remove old DeferredSetter implementation. Cleanup code, add license info. --- src/Avalonia.Base/AvaloniaObject.cs | 73 +------ ...ction.cs => AvaloniaPropertyValueStore.cs} | 13 +- src/Avalonia.Base/Utilities/DeferredSetter.cs | 180 +++++------------- .../Utilities/DeferredSetterOptimized.cs | 81 -------- src/Avalonia.Base/ValueStore.cs | 14 +- .../Base/DirectPropertyBenchmark.cs | 17 -- 6 files changed, 66 insertions(+), 312 deletions(-) rename src/Avalonia.Base/Utilities/{AvaloniaPropertyCollection.cs => AvaloniaPropertyValueStore.cs} (91%) delete mode 100644 src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 99e27e7daf..85dccd53cb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -508,45 +508,6 @@ namespace Avalonia } } - /// - /// A callback type for encapsulating complex logic for setting direct properties. - /// - /// The type of the property. - /// The value to which to set the property. - /// The backing field for the property. - /// A wrapper for the property-changed notification. - protected delegate void SetAndRaiseCallback(T value, ref T field, Action notifyWrapper); - - /// - /// Sets the backing field for a direct avalonia property, raising the - /// event if the value has changed. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// A callback called to actually set the value to the backing field. - /// The value. - /// - /// True if the value changed, otherwise false. - /// - protected bool SetAndRaise( - AvaloniaProperty property, - ref T field, - SetAndRaiseCallback setterCallback, - T value) - { - Contract.Requires(setterCallback != null); - return Values.Setter.SetAndNotify( - property, - ref field, - (object update, ref T backing, Action notify) => - { - setterCallback((T)update, ref backing, notify); - return true; - }, - value); - } - /// /// Sets the backing field for a direct avalonia property, raising the /// event if the value has changed. @@ -562,48 +523,16 @@ namespace Avalonia { VerifyAccess(); - return SetAndRaise( - property, - ref field, - (T val, ref T backing, Action notifyWrapper) - => SetAndRaiseCore(property, ref backing, val, notifyWrapper), - value); - } - - protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) - { - VerifyAccess(); - if (EqualityComparer.Default.Equals(field, value)) { return false; } - DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + DeferredSetter setter = Values.GetDeferredSetter(property); return setter.SetAndNotify(this, property, ref field, value); } - /// - /// Default assignment logic for SetAndRaise. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) - { - var old = field; - field = value; - - notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue)); - return true; - } - /// /// Tries to cast a value to a type, taking into account that the value may be a /// . diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs similarity index 91% rename from src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs rename to src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index 9b64342814..ac128d83de 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -1,13 +1,20 @@ -using System; +// 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; namespace Avalonia.Utilities { - internal sealed class AvaloniaPropertyCollection + /// + /// Stores values with as key. + /// + /// Stored value type. + internal sealed class AvaloniaPropertyValueStore { private Entry[] _entries; - public AvaloniaPropertyCollection() + public AvaloniaPropertyValueStore() { // The last item in the list is always int.MaxValue _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 1b1324b1c5..54458d6e6a 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -1,5 +1,7 @@ -using System; -using System.Collections.Generic; +// 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; namespace Avalonia.Utilities { @@ -8,161 +10,75 @@ namespace Avalonia.Utilities /// Used to fix #855. /// /// The type of value with which to track the delayed assignment. - class DeferredSetter + internal sealed class DeferredSetter { - private struct NotifyDisposable : IDisposable + private readonly SingleOrQueue _pendingValues; + private bool _isNotifying; + + public DeferredSetter() { - private readonly SettingStatus status; + _pendingValues = new SingleOrQueue(); + } - internal NotifyDisposable(SettingStatus status) - { - this.status = status; - status.Notifying = true; - } + private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) + { + var old = backing; - public void Dispose() - { - status.Notifying = false; - } + backing = value; + + source.RaisePropertyChanged(property, old, value); } - /// - /// Information on current setting/notification status of a property. - /// - private class SettingStatus + public bool SetAndNotify( + AvaloniaObject source, + AvaloniaProperty property, + ref TSetRecord backing, + TSetRecord value) { - public bool Notifying { get; set; } - - private SingleOrQueue pendingValues; - - public SingleOrQueue PendingValues + if (!_isNotifying) { - get + using (new NotifyDisposable(this)) { - return pendingValues ?? (pendingValues = new SingleOrQueue()); + SetAndRaisePropertyChanged(source, property, ref backing, value); } - } - } - private Dictionary _setRecords; - private Dictionary SetRecords - => _setRecords ?? (_setRecords = new Dictionary()); + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); + } + } + } - private SettingStatus GetOrCreateStatus(AvaloniaProperty property) - { - if (!SetRecords.TryGetValue(property, out var status)) - { - status = new SettingStatus(); - SetRecords.Add(property, status); + return true; } - return status; - } - - /// - /// Mark the property as currently notifying. - /// - /// The property to mark as notifying. - /// Returns a disposable that when disposed, marks the property as done notifying. - private NotifyDisposable MarkNotifying(AvaloniaProperty property) - { - Contract.Requires(!IsNotifying(property)); - - SettingStatus status = GetOrCreateStatus(property); - - return new NotifyDisposable(status); - } - - /// - /// Check if the property is currently notifying listeners. - /// - /// The property. - /// If the property is currently notifying listeners. - private bool IsNotifying(AvaloniaProperty property) - => SetRecords.TryGetValue(property, out var value) && value.Notifying; - - /// - /// Add a pending assignment for the property. - /// - /// The property. - /// The value to assign. - private void AddPendingSet(AvaloniaProperty property, TSetRecord value) - { - Contract.Requires(IsNotifying(property)); - - GetOrCreateStatus(property).PendingValues.Enqueue(value); - } + _pendingValues.Enqueue(value); - /// - /// Checks if there are any pending assignments for the property. - /// - /// The property to check. - /// If the property has any pending assignments. - private bool HasPendingSet(AvaloniaProperty property) - { - return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty; + return false; } /// - /// Gets the first pending assignment for the property. + /// Disposable that marks the property as currently notifying. + /// When disposed, marks the property as done notifying. /// - /// The property to check. - /// The first pending assignment for the property. - private TSetRecord GetFirstPendingSet(AvaloniaProperty property) + private readonly struct NotifyDisposable : IDisposable { - return GetOrCreateStatus(property).PendingValues.Dequeue(); - } - - public delegate bool SetterDelegate(TSetRecord record, ref TValue backing, Action notifyCallback); + private readonly DeferredSetter _setter; - /// - /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824 - /// - /// The property to set. - /// The backing field for the property - /// - /// A callback that actually sets the property. - /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification. - /// - /// The value to try to set. - public bool SetAndNotify( - AvaloniaProperty property, - ref TValue backing, - SetterDelegate setterCallback, - TSetRecord value) - { - Contract.Requires(setterCallback != null); - if (!IsNotifying(property)) + internal NotifyDisposable(DeferredSetter setter) { - bool updated = false; - if (!object.Equals(value, backing)) - { - updated = setterCallback(value, ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - while (HasPendingSet(property)) - { - updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - - return updated; + _setter = setter; + _setter._isNotifying = true; } - else if(!object.Equals(value, backing)) + + public void Dispose() { - AddPendingSet(property, value); + _setter._isNotifying = false; } - return false; } } } diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs deleted file mode 100644 index 4858372828..0000000000 --- a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; - -namespace Avalonia.Utilities -{ - /// - /// A utility class to enable deferring assignment until after property-changed notifications are sent. - /// Used to fix #855. - /// - /// The type of value with which to track the delayed assignment. - internal sealed class DeferredSetterOptimized - { - private bool _isNotifying; - private readonly SingleOrQueue _pendingValues; - - public DeferredSetterOptimized() - { - _pendingValues = new SingleOrQueue(); - } - - private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) - { - var old = backing; - - backing = value; - - source.RaisePropertyChanged(property, old, value); - } - - public bool SetAndNotify( - AvaloniaObject source, - AvaloniaProperty property, - ref TSetRecord backing, - TSetRecord value) - { - if (!_isNotifying) - { - using (new NotifyDisposable(this)) - { - SetAndRaisePropertyChanged(source, property, ref backing, value); - } - - if (!_pendingValues.Empty) - { - using (new NotifyDisposable(this)) - { - while (!_pendingValues.Empty) - { - SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); - } - } - } - - return true; - } - - _pendingValues.Enqueue(value); - - return false; - } - - /// - /// Disposable that marks the property as currently notifying. - /// When disposed, marks the property as done notifying. - /// - private readonly struct NotifyDisposable : IDisposable - { - private readonly DeferredSetterOptimized _setter; - - internal NotifyDisposable(DeferredSetterOptimized setter) - { - _setter = setter; - _setter._isNotifying = true; - } - - public void Dispose() - { - _setter._isNotifying = false; - } - } - } -} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index b7f5c26801..8dfabc71a2 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,15 +7,15 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - private readonly AvaloniaPropertyCollection _propertyValues; - private readonly AvaloniaPropertyCollection _deferredSetters; + private readonly AvaloniaPropertyValueStore _propertyValues; + private readonly AvaloniaPropertyValueStore _deferredSetters; private readonly AvaloniaObject _owner; public ValueStore(AvaloniaObject owner) { _owner = owner; - _propertyValues = new AvaloniaPropertyCollection(); - _deferredSetters = new AvaloniaPropertyCollection(); + _propertyValues = new AvaloniaPropertyValueStore(); + _deferredSetters = new AvaloniaPropertyValueStore(); } public IDisposable AddBinding( @@ -178,14 +178,14 @@ namespace Avalonia return value; } - public DeferredSetterOptimized GetDeferredSetter(AvaloniaProperty property) + public DeferredSetter GetDeferredSetter(AvaloniaProperty property) { if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - return (DeferredSetterOptimized)deferredSetter; + return (DeferredSetter)deferredSetter; } - var newDeferredSetter = new DeferredSetterOptimized(); + var newDeferredSetter = new DeferredSetter(); _deferredSetters.AddValue(property, newDeferredSetter); diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs index ecf8a1fb2b..02f9397e2c 100644 --- a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -16,17 +16,6 @@ namespace Avalonia.Benchmarks.Base } } - [Benchmark] - public void SetAndRaiseOptimized() - { - var obj = new DirectClass(); - - for (var i = 0; i < 100; ++i) - { - obj.IntValueOptimized += 1; - } - } - [Benchmark] public void SetAndRaiseSimple() { @@ -53,12 +42,6 @@ namespace Avalonia.Benchmarks.Base set => SetAndRaise(IntValueProperty, ref _intValue, value); } - public int IntValueOptimized - { - get => _intValue; - set => SetAndRaiseOptimized(IntValueProperty, ref _intValue, value); - } - public int IntValueSimple { get => _intValue; From ad994a685cd6a8778bfebc6bcc77fce2fbd9a704 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 22 Jul 2019 00:19:45 +0200 Subject: [PATCH 4/4] Fix PriorityValue usage of DeferredSetter. Implement slower path in DeferredSetter that deals with callbacks. --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- src/Avalonia.Base/IPriorityValueOwner.cs | 9 ++++- src/Avalonia.Base/PriorityValue.cs | 34 +++++++++-------- src/Avalonia.Base/Utilities/DeferredSetter.cs | 38 +++++++++++++++++++ src/Avalonia.Base/ValueStore.cs | 15 ++++---- .../PriorityValueTests.cs | 2 +- 6 files changed, 72 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 85dccd53cb..a3d5803ab0 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -528,7 +528,7 @@ namespace Avalonia return false; } - DeferredSetter setter = Values.GetDeferredSetter(property); + DeferredSetter setter = Values.GetDirectDeferredSetter(property); return setter.SetAndNotify(this, property, ref field, value); } diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 540b1bf19b..1d6e5e59ad 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -29,6 +29,13 @@ namespace Avalonia /// The notification. void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); + /// + /// Returns deferred setter for given non-direct property. + /// + /// Property. + /// Deferred setter for given property. + DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property); + /// /// Logs a binding error. /// @@ -40,7 +47,5 @@ namespace Avalonia /// Ensures that the current thread is the UI thread. /// void VerifyAccess(); - - DeferredSetter Setter { get; } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 89a893577f..4996420fe7 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -30,7 +30,9 @@ namespace Avalonia private readonly SingleOrDictionary _levels = new SingleOrDictionary(); private readonly Func _validate; + private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback; private (object value, int priority) _value; + private DeferredSetter _setter; /// /// Initializes a new instance of the class. @@ -50,6 +52,7 @@ namespace Avalonia _valueType = valueType; _value = (AvaloniaProperty.UnsetValue, int.MaxValue); _validate = validate; + _setAndNotifyCallback = SetAndNotify; } /// @@ -242,22 +245,22 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - Owner.Setter.SetAndNotify(Property, - ref _value, - UpdateCore, - (value, priority)); - } + var newValue = (value, priority); + + if (newValue == _value) + { + return; + } - private bool UpdateCore( - object update, - ref (object value, int priority) backing, - Action notify) - => UpdateCore(((object, int))update, ref backing, notify); + if (_setter == null) + { + _setter = Owner.GetNonDirectDeferredSetter(Property); + } + + _setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue); + } - private bool UpdateCore( - (object value, int priority) update, - ref (object value, int priority) backing, - Action notify) + private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update) { var val = update.value; var notification = val as BindingNotification; @@ -286,7 +289,7 @@ namespace Avalonia if (notification == null || notification.HasValue) { - notify(() => Owner?.Changed(Property, ValuePriority, old, Value)); + Owner?.Changed(Property, ValuePriority, old, Value); } if (notification != null) @@ -305,7 +308,6 @@ namespace Avalonia val, val?.GetType()); } - return true; } } } diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 54458d6e6a..fd7a66fb52 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -5,6 +5,15 @@ using System; namespace Avalonia.Utilities { + /// + /// Callback invoked when deferred setter wants to set a value. + /// + /// Value type. + /// Property being set. + /// Backing field reference. + /// New value. + internal delegate void SetAndNotifyCallback(AvaloniaProperty property, ref TValue backing, TValue value); + /// /// A utility class to enable deferring assignment until after property-changed notifications are sent. /// Used to fix #855. @@ -61,6 +70,35 @@ namespace Avalonia.Utilities return false; } + public bool SetAndNotifyCallback(AvaloniaProperty property, SetAndNotifyCallback setAndNotifyCallback, ref TValue backing, TValue value) + where TValue : TSetRecord + { + if (!_isNotifying) + { + using (new NotifyDisposable(this)) + { + setAndNotifyCallback(property, ref backing, value); + } + + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); + } + } + } + + return true; + } + + _pendingValues.Enqueue(value); + + return false; + } + /// /// Disposable that marks the property as currently notifying. /// When disposed, marks the property as done notifying. diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 8dfabc71a2..1bdbd4ca7c 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -178,7 +178,7 @@ namespace Avalonia return value; } - public DeferredSetter GetDeferredSetter(AvaloniaProperty property) + private DeferredSetter GetDeferredSetter(AvaloniaProperty property) { if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { @@ -192,15 +192,14 @@ namespace Avalonia return newDeferredSetter; } - private DeferredSetter _deferredSetter; + public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property) + { + return GetDeferredSetter(property); + } - public DeferredSetter Setter + public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property) { - get - { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); - } + return GetDeferredSetter(property); } } } diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 2f1b7862a7..63e1790cce 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests private static Mock GetMockOwner() { var owner = new Mock(); - owner.SetupGet(o => o.Setter).Returns(new DeferredSetter()); + owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny())).Returns(new DeferredSetter()); return owner; } }