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; } } }