Skip to content
51 changes: 13 additions & 38 deletions src/Avalonia.Base/AvaloniaObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Avalonia
/// <remarks>
/// This class is analogous to DependencyObject in WPF.
/// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
{
/// <summary>
/// The parent object that inherited values are inherited from.
Expand All @@ -45,21 +45,8 @@ public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyProp
/// </summary>
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;

private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
private ValueStore _values;

/// <summary>
/// Delayed setter helper for direct properties. Used to fix #855.
/// </summary>
private DeferredSetter<AvaloniaProperty, object> DirectPropertyDeferredSetter
{
get
{
return _directDeferredSetter ??
(_directDeferredSetter = new DeferredSetter<AvaloniaProperty, object>());
}
}

private ValueStore Values => _values ?? (_values = new ValueStore(this));

/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
Expand Down Expand Up @@ -225,7 +212,7 @@ public object GetValue(AvaloniaProperty property)
}
else if (_values != null)
{
var result = _values.GetValue(property);
var result = Values.GetValue(property);

if (result == AvaloniaProperty.UnsetValue)
{
Expand Down Expand Up @@ -376,12 +363,7 @@ public IDisposable Bind(
description,
priority);

if (_values == null)
{
_values = new ValueStore(this);
}

return _values.AddBinding(property, source, priority);
return Values.AddBinding(property, source, priority);
}
}

Expand Down Expand Up @@ -414,9 +396,8 @@ public void Revalidate(AvaloniaProperty property)
VerifyAccess();
_values?.Revalidate(property);
}

/// <inheritdoc/>
void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)

internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
{
oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
GetDefaultValue(property) :
Expand All @@ -439,9 +420,8 @@ void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object
(BindingPriority)priority);
}
}

/// <inheritdoc/>
void IPriorityValueOwner.BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)

internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
UpdateDataValidation(property, notification);
}
Expand All @@ -456,7 +436,7 @@ Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
/// Gets all priority values set on the object.
/// </summary>
/// <returns>A collection of property/value tuples.</returns>
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => _values?.GetSetValues();
internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => Values?.GetSetValues();

/// <summary>
/// Forces revalidation of properties when a property value changes.
Expand Down Expand Up @@ -566,12 +546,12 @@ protected bool SetAndRaise<T>(
T value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
return DirectPropertyDeferredSetter.SetAndNotify(
return Values.Setter.SetAndNotify(
property,
ref field,
(object val, ref T backing, Action<Action> notify) =>
(object update, ref T backing, Action<Action> notify) =>
{
setterCallback((T)val, ref backing, notify);
setterCallback((T)update, ref backing, notify);
return true;
},
value);
Expand Down Expand Up @@ -737,13 +717,8 @@ private void SetStyledValue(AvaloniaProperty property, object value, BindingPrio
originalValue?.GetType().FullName ?? "(null)"));
}

if (_values == null)
{
_values = new ValueStore(this);
}

LogPropertySet(property, value, priority);
_values.AddValue(property, value, (int)priority);
Values.AddValue(property, value, (int)priority);
}

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions src/Avalonia.Base/IPriorityValueOwner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.

using Avalonia.Data;
using Avalonia.Utilities;

namespace Avalonia
{
Expand Down Expand Up @@ -31,5 +32,7 @@ internal interface IPriorityValueOwner
/// Ensures that the current thread is the UI thread.
/// </summary>
void VerifyAccess();

DeferredSetter<object> Setter { get; }
}
}
11 changes: 8 additions & 3 deletions src/Avalonia.Base/PriorityValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Avalonia
/// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
/// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes, the
/// <see cref="IPriorityValueOwner.Changed(PriorityValue, object, object)"/> method on the
/// <see cref="IPriorityValueOwner.Changed"/> method on the
/// owner object is fired with the old and new values.
/// </remarks>
internal class PriorityValue
Expand All @@ -30,7 +30,6 @@ internal class PriorityValue
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();

private readonly Func<object, object> _validate;
private static readonly DeferredSetter<PriorityValue, (object value, int priority)> delayedSetter = new DeferredSetter<PriorityValue, (object, int)>();
private (object value, int priority) _value;

/// <summary>
Expand Down Expand Up @@ -243,12 +242,18 @@ private PriorityLevel GetLevel(int priority)
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
delayedSetter.SetAndNotify(this,
Owner.Setter.SetAndNotify(Property,
ref _value,
UpdateCore,
(value, priority));
}

private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action<Action> notify)
=> UpdateCore(((object, int))update, ref backing, notify);

private bool UpdateCore(
(object value, int priority) update,
ref (object value, int priority) backing,
Expand Down
53 changes: 34 additions & 19 deletions src/Avalonia.Base/Utilities/DeferredSetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ 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="TProperty">The type of the object that represents the property.</typeparam>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
class DeferredSetter<TProperty, TSetRecord>
where TProperty: class
class DeferredSetter<TSetRecord>
{
private struct NotifyDisposable : IDisposable
{
Expand All @@ -37,69 +36,84 @@ private class SettingStatus
{
public bool Notifying { get; set; }

private Queue<TSetRecord> pendingValues;
private SingleOrQueue<TSetRecord> pendingValues;

public Queue<TSetRecord> PendingValues
public SingleOrQueue<TSetRecord> PendingValues
{
get
{
return pendingValues ?? (pendingValues = new Queue<TSetRecord>());
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
}
}
}

private readonly ConditionalWeakTable<TProperty, SettingStatus> setRecords = new ConditionalWeakTable<TProperty, SettingStatus>();
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());

private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
}

return status;
}

/// <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(TProperty property)
private NotifyDisposable MarkNotifying(AvaloniaProperty property)
{
Contract.Requires<InvalidOperationException>(!IsNotifying(property));

return new NotifyDisposable(setRecords.GetOrCreateValue(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(TProperty property)
=> setRecords.TryGetValue(property, out var value) && value.Notifying;
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(TProperty property, TSetRecord value)
private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
{
Contract.Requires<InvalidOperationException>(IsNotifying(property));

setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value);
GetOrCreateStatus(property).PendingValues.Enqueue(value);
}

/// <summary>
/// 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(TProperty property)
private bool HasPendingSet(AvaloniaProperty property)
{
return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0;
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
}

/// <summary>
/// Gets the first pending assignment for the property.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(TProperty property)
private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
{
return setRecords.GetOrCreateValue(property).PendingValues.Dequeue();
return GetOrCreateStatus(property).PendingValues.Dequeue();
}

public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
Expand All @@ -115,7 +129,7 @@ private TSetRecord GetFirstPendingSet(TProperty property)
/// </param>
/// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>(
TProperty property,
AvaloniaProperty property,
ref TValue backing,
SetterDelegate<TValue> setterCallback,
TSetRecord value)
Expand Down Expand Up @@ -144,6 +158,7 @@ public bool SetAndNotify<TValue>(
}
});
}

return updated;
}
else if(!object.Equals(value, backing))
Expand Down
58 changes: 58 additions & 0 deletions src/Avalonia.Base/Utilities/SingleOrQueue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Avalonia.Utilities
{
/// <summary>
/// FIFO Queue optimized for holding zero or one items.
/// </summary>
/// <typeparam name="T">The type of items held in the queue.</typeparam>
public class SingleOrQueue<T>
{
private T _head;
private Queue<T> _tail;

private Queue<T> Tail => _tail ?? (_tail = new Queue<T>());

private bool HasTail => _tail != null;

public bool Empty { get; private set; } = true;

public void Enqueue(T value)
{
if (Empty)
{
_head = value;
}
else
{
Tail.Enqueue(value);
}

Empty = false;
}

public T Dequeue()
{
if (Empty)
{
throw new InvalidOperationException("Cannot dequeue from an empty queue!");
}

var result = _head;

if (HasTail && Tail.Count != 0)
{
_head = Tail.Dequeue();
}
else
{
_head = default;
Empty = true;
}

return result;
}
}
}
Loading