DeferredSetter.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Reactive.Disposables;
  4. using System.Runtime.CompilerServices;
  5. using System.Text;
  6. namespace Avalonia.Utilities
  7. {
  8. /// <summary>
  9. /// A utility class to enable deferring assignment until after property-changed notifications are sent.
  10. /// Used to fix #855.
  11. /// </summary>
  12. /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
  13. class DeferredSetter<TSetRecord>
  14. {
  15. private struct NotifyDisposable : IDisposable
  16. {
  17. private readonly SettingStatus status;
  18. internal NotifyDisposable(SettingStatus status)
  19. {
  20. this.status = status;
  21. status.Notifying = true;
  22. }
  23. public void Dispose()
  24. {
  25. status.Notifying = false;
  26. }
  27. }
  28. /// <summary>
  29. /// Information on current setting/notification status of a property.
  30. /// </summary>
  31. private class SettingStatus
  32. {
  33. public bool Notifying { get; set; }
  34. private SingleOrQueue<TSetRecord> pendingValues;
  35. public SingleOrQueue<TSetRecord> PendingValues
  36. {
  37. get
  38. {
  39. return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
  40. }
  41. }
  42. public bool IsSimpleSet => pendingValues?.HasTail != true;
  43. }
  44. private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
  45. private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
  46. => _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
  47. private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
  48. {
  49. if (!SetRecords.TryGetValue(property, out var status))
  50. {
  51. status = new SettingStatus();
  52. SetRecords.Add(property, status);
  53. }
  54. return status;
  55. }
  56. /// <summary>
  57. /// Mark the property as currently notifying.
  58. /// </summary>
  59. /// <param name="property">The property to mark as notifying.</param>
  60. /// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
  61. private NotifyDisposable MarkNotifying(AvaloniaProperty property)
  62. {
  63. Contract.Requires<InvalidOperationException>(!IsNotifying(property));
  64. SettingStatus status = GetOrCreateStatus(property);
  65. return new NotifyDisposable(status);
  66. }
  67. /// <summary>
  68. /// Check if the property is currently notifying listeners.
  69. /// </summary>
  70. /// <param name="property">The property.</param>
  71. /// <returns>If the property is currently notifying listeners.</returns>
  72. private bool IsNotifying(AvaloniaProperty property)
  73. => SetRecords.TryGetValue(property, out var value) && value.Notifying;
  74. /// <summary>
  75. /// Add a pending assignment for the property.
  76. /// </summary>
  77. /// <param name="property">The property.</param>
  78. /// <param name="value">The value to assign.</param>
  79. private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
  80. {
  81. Contract.Requires<InvalidOperationException>(IsNotifying(property));
  82. GetOrCreateStatus(property).PendingValues.Enqueue(value);
  83. }
  84. /// <summary>
  85. /// Checks if there are any pending assignments for the property.
  86. /// </summary>
  87. /// <param name="property">The property to check.</param>
  88. /// <returns>If the property has any pending assignments.</returns>
  89. private bool HasPendingSet(AvaloniaProperty property)
  90. {
  91. return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
  92. }
  93. /// <summary>
  94. /// Gets the first pending assignment for the property.
  95. /// </summary>
  96. /// <param name="property">The property to check.</param>
  97. /// <returns>The first pending assignment for the property.</returns>
  98. private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
  99. {
  100. return GetOrCreateStatus(property).PendingValues.Dequeue();
  101. }
  102. private void CleanupSetStatus(AvaloniaProperty property)
  103. {
  104. if (SetRecords.TryGetValue(property, out var status) && status.IsSimpleSet)
  105. {
  106. SetRecords.Remove(property);
  107. }
  108. }
  109. public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
  110. /// <summary>
  111. /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
  112. /// </summary>
  113. /// <param name="property">The property to set.</param>
  114. /// <param name="backing">The backing field for the property</param>
  115. /// <param name="setterCallback">
  116. /// A callback that actually sets the property.
  117. /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
  118. /// </param>
  119. /// <param name="value">The value to try to set.</param>
  120. public bool SetAndNotify<TValue>(
  121. AvaloniaProperty property,
  122. ref TValue backing,
  123. SetterDelegate<TValue> setterCallback,
  124. TSetRecord value)
  125. {
  126. Contract.Requires<ArgumentNullException>(setterCallback != null);
  127. if (!IsNotifying(property))
  128. {
  129. bool updated = false;
  130. if (!object.Equals(value, backing))
  131. {
  132. updated = setterCallback(value, ref backing, notification =>
  133. {
  134. using (MarkNotifying(property))
  135. {
  136. notification();
  137. }
  138. });
  139. }
  140. while (HasPendingSet(property))
  141. {
  142. updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification =>
  143. {
  144. using (MarkNotifying(property))
  145. {
  146. notification();
  147. }
  148. });
  149. }
  150. CleanupSetStatus(property);
  151. return updated;
  152. }
  153. else if(!object.Equals(value, backing))
  154. {
  155. AddPendingSet(property, value);
  156. }
  157. return false;
  158. }
  159. }
  160. }