using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Runtime.CompilerServices;
using System.Text;
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.
class DeferredSetter
{
private struct NotifyDisposable : IDisposable
{
private readonly SettingStatus status;
internal NotifyDisposable(SettingStatus status)
{
this.status = status;
status.Notifying = true;
}
public void Dispose()
{
status.Notifying = false;
}
}
///
/// Information on current setting/notification status of a property.
///
private class SettingStatus
{
public bool Notifying { get; set; }
private SingleOrQueue pendingValues;
public SingleOrQueue PendingValues
{
get
{
return pendingValues ?? (pendingValues = new SingleOrQueue());
}
}
public bool IsSimpleSet => pendingValues?.HasTail != true;
}
private Dictionary _setRecords;
private Dictionary SetRecords
=> _setRecords ?? (_setRecords = new Dictionary());
private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
}
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);
}
///
/// 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;
}
///
/// Gets the first pending assignment for the property.
///
/// The property to check.
/// The first pending assignment for the property.
private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
{
return GetOrCreateStatus(property).PendingValues.Dequeue();
}
private void CleanupSetStatus(AvaloniaProperty property)
{
if (SetRecords.TryGetValue(property, out var status) && status.IsSimpleSet)
{
SetRecords.Remove(property);
}
}
public delegate bool SetterDelegate(TSetRecord record, ref TValue backing, Action notifyCallback);
///
/// 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))
{
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();
}
});
}
CleanupSetStatus(property);
return updated;
}
else if(!object.Equals(value, backing))
{
AddPendingSet(property, value);
}
return false;
}
}
}