diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7601b64ce9..a3d5803ab0 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, @@ -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. @@ -561,32 +522,15 @@ namespace Avalonia protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) { VerifyAccess(); - return SetAndRaise( - property, - ref field, - (T val, ref T backing, Action notifyWrapper) - => SetAndRaiseCore(property, ref backing, val, notifyWrapper), - 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; + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetter setter = Values.GetDirectDeferredSetter(property); - notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue)); - return true; + 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/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs new file mode 100644 index 0000000000..ac128d83de --- /dev/null +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -0,0 +1,150 @@ +// 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 +{ + /// + /// Stores values with as key. + /// + /// Stored value type. + internal sealed class AvaloniaPropertyValueStore + { + private Entry[] _entries; + + public AvaloniaPropertyValueStore() + { + // 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 AddValue(AvaloniaProperty property, TValue value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + public void SetValue(AvaloniaProperty property, TValue value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + public 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/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 1b1324b1c5..fd7a66fb52 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -1,168 +1,122 @@ -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 { + /// + /// 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. /// /// 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; + _pendingValues.Enqueue(value); + + return false; } - /// - /// 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) + public bool SetAndNotifyCallback(AvaloniaProperty property, SetAndNotifyCallback setAndNotifyCallback, ref TValue backing, TValue value) + where TValue : TSetRecord { - Contract.Requires(!IsNotifying(property)); - - SettingStatus status = GetOrCreateStatus(property); - - return new NotifyDisposable(status); - } + if (!_isNotifying) + { + using (new NotifyDisposable(this)) + { + setAndNotifyCallback(property, ref backing, value); + } - /// - /// 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; + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); + } + } + } - /// - /// Add a pending assignment for the property. - /// - /// The property. - /// The value to assign. - private void AddPendingSet(AvaloniaProperty property, TSetRecord value) - { - Contract.Requires(IsNotifying(property)); + return true; + } - 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/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 24f85ea6b1..1bdbd4ca7c 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 AvaloniaPropertyValueStore _propertyValues; + private readonly AvaloniaPropertyValueStore _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 AvaloniaPropertyValueStore(); + _deferredSetters = new AvaloniaPropertyValueStore(); } 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.SetValue(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValue(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.SetValue(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.SetValue(property, priorityValue); } } } @@ -84,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValue(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValue(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,28 @@ namespace Avalonia return value; } - private DeferredSetter _deferredSetter; - - public DeferredSetter Setter + private DeferredSetter GetDeferredSetter(AvaloniaProperty property) { - get + if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); + return (DeferredSetter)deferredSetter; } - } - private bool TryGetValue(AvaloniaProperty property, out object value) - { - (int index, bool found) = TryFindEntry(property.Id); - if (!found) - { - value = null; - return false; - } - - 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 DeferredSetter(); - entries[i] = new Entry { PropertyId = property.Id, Value = value }; - Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); - break; - } - } + _deferredSetters.AddValue(property, newDeferredSetter); - _entries = entries; + return newDeferredSetter; } - private void SetValueInternal(AvaloniaProperty property, object value) + public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property) { - _entries[TryFindEntry(property.Id).Item1].Value = value; + return GetDeferredSetter(property); } - private (int, bool) TryFindEntry(int propertyId) + public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property) { - 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); + 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; } } 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 new file mode 100644 index 0000000000..02f9397e2c --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -0,0 +1,65 @@ +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 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 IntValueSimple + { + get => _intValue; + set + { + VerifyAccess(); + + if (_intValue == value) + { + return; + } + + var old = _intValue; + _intValue = value; + + RaisePropertyChanged(IntValueProperty, old, _intValue); + } + } + } + } +} 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