Browse Source

Remove old DeferredSetter implementation. Cleanup code, add license info.

pull/2362/head
Dariusz Komosinski 7 years ago
parent
commit
48be9dc260
  1. 73
      src/Avalonia.Base/AvaloniaObject.cs
  2. 13
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  3. 180
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  4. 81
      src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs
  5. 14
      src/Avalonia.Base/ValueStore.cs
  6. 17
      tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs

73
src/Avalonia.Base/AvaloniaObject.cs

@ -508,45 +508,6 @@ namespace Avalonia
} }
} }
/// <summary>
/// A callback type for encapsulating complex logic for setting direct properties.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="value">The value to which to set the property.</param>
/// <param name="field">The backing field for the property.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
protected delegate void SetAndRaiseCallback<T>(T value, ref T field, Action<Action> notifyWrapper);
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="setterCallback">A callback called to actually set the value to the backing field.</param>
/// <param name="value">The value.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
protected bool SetAndRaise<T>(
AvaloniaProperty<T> property,
ref T field,
SetAndRaiseCallback<T> setterCallback,
T value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
return Values.Setter.SetAndNotify(
property,
ref field,
(object update, ref T backing, Action<Action> notify) =>
{
setterCallback((T)update, ref backing, notify);
return true;
},
value);
}
/// <summary> /// <summary>
/// Sets the backing field for a direct avalonia property, raising the /// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed. /// <see cref="PropertyChanged"/> event if the value has changed.
@ -562,48 +523,16 @@ namespace Avalonia
{ {
VerifyAccess(); VerifyAccess();
return SetAndRaise(
property,
ref field,
(T val, ref T backing, Action<Action> notifyWrapper)
=> SetAndRaiseCore(property, ref backing, val, notifyWrapper),
value);
}
protected bool SetAndRaiseOptimized<T>(AvaloniaProperty<T> property, ref T field, T value)
{
VerifyAccess();
if (EqualityComparer<T>.Default.Equals(field, value)) if (EqualityComparer<T>.Default.Equals(field, value))
{ {
return false; return false;
} }
DeferredSetterOptimized<T> setter = Values.GetDeferredSetter(property); DeferredSetter<T> setter = Values.GetDeferredSetter(property);
return setter.SetAndNotify(this, property, ref field, value); return setter.SetAndNotify(this, property, ref field, value);
} }
/// <summary>
/// Default assignment logic for SetAndRaise.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="value">The value.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
private bool SetAndRaiseCore<T>(AvaloniaProperty property, ref T field, T value, Action<Action> notifyWrapper)
{
var old = field;
field = value;
notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue));
return true;
}
/// <summary> /// <summary>
/// Tries to cast a value to a type, taking into account that the value may be a /// Tries to cast a value to a type, taking into account that the value may be a
/// <see cref="BindingNotification"/>. /// <see cref="BindingNotification"/>.

13
src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs → 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; using System.Collections.Generic;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
internal sealed class AvaloniaPropertyCollection<TValue> /// <summary>
/// Stores values with <see cref="AvaloniaProperty"/> as key.
/// </summary>
/// <typeparam name="TValue">Stored value type.</typeparam>
internal sealed class AvaloniaPropertyValueStore<TValue>
{ {
private Entry[] _entries; private Entry[] _entries;
public AvaloniaPropertyCollection() public AvaloniaPropertyValueStore()
{ {
// The last item in the list is always int.MaxValue // The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };

180
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -1,5 +1,7 @@
using System; // Copyright (c) The Avalonia Project. All rights reserved.
using System.Collections.Generic; // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
@ -8,161 +10,75 @@ namespace Avalonia.Utilities
/// Used to fix #855. /// Used to fix #855.
/// </summary> /// </summary>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam> /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
class DeferredSetter<TSetRecord> internal sealed class DeferredSetter<TSetRecord>
{ {
private struct NotifyDisposable : IDisposable private readonly SingleOrQueue<TSetRecord> _pendingValues;
private bool _isNotifying;
public DeferredSetter()
{ {
private readonly SettingStatus status; _pendingValues = new SingleOrQueue<TSetRecord>();
}
internal NotifyDisposable(SettingStatus status) private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value)
{ {
this.status = status; var old = backing;
status.Notifying = true;
}
public void Dispose() backing = value;
{
status.Notifying = false; source.RaisePropertyChanged(property, old, value);
}
} }
/// <summary> public bool SetAndNotify(
/// Information on current setting/notification status of a property. AvaloniaObject source,
/// </summary> AvaloniaProperty<TSetRecord> property,
private class SettingStatus ref TSetRecord backing,
TSetRecord value)
{ {
public bool Notifying { get; set; } if (!_isNotifying)
private SingleOrQueue<TSetRecord> pendingValues;
public SingleOrQueue<TSetRecord> PendingValues
{ {
get using (new NotifyDisposable(this))
{ {
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>()); SetAndRaisePropertyChanged(source, property, ref backing, value);
} }
}
}
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords; if (!_pendingValues.Empty)
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords {
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>()); using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue());
}
}
}
private SettingStatus GetOrCreateStatus(AvaloniaProperty property) return true;
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
} }
return status; _pendingValues.Enqueue(value);
}
/// <summary>
/// Mark the property as currently notifying.
/// </summary>
/// <param name="property">The property to mark as notifying.</param>
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
private NotifyDisposable MarkNotifying(AvaloniaProperty property)
{
Contract.Requires<InvalidOperationException>(!IsNotifying(property));
SettingStatus status = GetOrCreateStatus(property);
return new NotifyDisposable(status);
}
/// <summary>
/// Check if the property is currently notifying listeners.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>If the property is currently notifying listeners.</returns>
private bool IsNotifying(AvaloniaProperty property)
=> SetRecords.TryGetValue(property, out var value) && value.Notifying;
/// <summary>
/// Add a pending assignment for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value to assign.</param>
private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
{
Contract.Requires<InvalidOperationException>(IsNotifying(property));
GetOrCreateStatus(property).PendingValues.Enqueue(value);
}
/// <summary> return false;
/// Checks if there are any pending assignments for the property.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(AvaloniaProperty property)
{
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
} }
/// <summary> /// <summary>
/// Gets the first pending assignment for the property. /// Disposable that marks the property as currently notifying.
/// When disposed, marks the property as done notifying.
/// </summary> /// </summary>
/// <param name="property">The property to check.</param> private readonly struct NotifyDisposable : IDisposable
/// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
{ {
return GetOrCreateStatus(property).PendingValues.Dequeue(); private readonly DeferredSetter<TSetRecord> _setter;
}
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
/// <summary> internal NotifyDisposable(DeferredSetter<TSetRecord> setter)
/// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
/// </summary>
/// <param name="property">The property to set.</param>
/// <param name="backing">The backing field for the property</param>
/// <param name="setterCallback">
/// 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.
/// </param>
/// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>(
AvaloniaProperty property,
ref TValue backing,
SetterDelegate<TValue> setterCallback,
TSetRecord value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
if (!IsNotifying(property))
{ {
bool updated = false; _setter = setter;
if (!object.Equals(value, backing)) _setter._isNotifying = true;
{
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;
} }
else if(!object.Equals(value, backing))
public void Dispose()
{ {
AddPendingSet(property, value); _setter._isNotifying = false;
} }
return false;
} }
} }
} }

81
src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs

@ -1,81 +0,0 @@
using System;
namespace Avalonia.Utilities
{
/// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
/// </summary>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
internal sealed class DeferredSetterOptimized<TSetRecord>
{
private bool _isNotifying;
private readonly SingleOrQueue<TSetRecord> _pendingValues;
public DeferredSetterOptimized()
{
_pendingValues = new SingleOrQueue<TSetRecord>();
}
private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value)
{
var old = backing;
backing = value;
source.RaisePropertyChanged(property, old, value);
}
public bool SetAndNotify(
AvaloniaObject source,
AvaloniaProperty<TSetRecord> 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;
}
/// <summary>
/// Disposable that marks the property as currently notifying.
/// When disposed, marks the property as done notifying.
/// </summary>
private readonly struct NotifyDisposable : IDisposable
{
private readonly DeferredSetterOptimized<TSetRecord> _setter;
internal NotifyDisposable(DeferredSetterOptimized<TSetRecord> setter)
{
_setter = setter;
_setter._isNotifying = true;
}
public void Dispose()
{
_setter._isNotifying = false;
}
}
}
}

14
src/Avalonia.Base/ValueStore.cs

@ -7,15 +7,15 @@ namespace Avalonia
{ {
internal class ValueStore : IPriorityValueOwner internal class ValueStore : IPriorityValueOwner
{ {
private readonly AvaloniaPropertyCollection<object> _propertyValues; private readonly AvaloniaPropertyValueStore<object> _propertyValues;
private readonly AvaloniaPropertyCollection<object> _deferredSetters; private readonly AvaloniaPropertyValueStore<object> _deferredSetters;
private readonly AvaloniaObject _owner; private readonly AvaloniaObject _owner;
public ValueStore(AvaloniaObject owner) public ValueStore(AvaloniaObject owner)
{ {
_owner = owner; _owner = owner;
_propertyValues = new AvaloniaPropertyCollection<object>(); _propertyValues = new AvaloniaPropertyValueStore<object>();
_deferredSetters = new AvaloniaPropertyCollection<object>(); _deferredSetters = new AvaloniaPropertyValueStore<object>();
} }
public IDisposable AddBinding( public IDisposable AddBinding(
@ -178,14 +178,14 @@ namespace Avalonia
return value; return value;
} }
public DeferredSetterOptimized<T> GetDeferredSetter<T>(AvaloniaProperty<T> property) public DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty<T> property)
{ {
if (_deferredSetters.TryGetValue(property, out var deferredSetter)) if (_deferredSetters.TryGetValue(property, out var deferredSetter))
{ {
return (DeferredSetterOptimized<T>)deferredSetter; return (DeferredSetter<T>)deferredSetter;
} }
var newDeferredSetter = new DeferredSetterOptimized<T>(); var newDeferredSetter = new DeferredSetter<T>();
_deferredSetters.AddValue(property, newDeferredSetter); _deferredSetters.AddValue(property, newDeferredSetter);

17
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] [Benchmark]
public void SetAndRaiseSimple() public void SetAndRaiseSimple()
{ {
@ -53,12 +42,6 @@ namespace Avalonia.Benchmarks.Base
set => SetAndRaise(IntValueProperty, ref _intValue, value); set => SetAndRaise(IntValueProperty, ref _intValue, value);
} }
public int IntValueOptimized
{
get => _intValue;
set => SetAndRaiseOptimized(IntValueProperty, ref _intValue, value);
}
public int IntValueSimple public int IntValueSimple
{ {
get => _intValue; get => _intValue;

Loading…
Cancel
Save