BehaviorCollection.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Specialized;
  4. using System.Diagnostics;
  5. using Avalonia.Collections;
  6. namespace Avalonia.Xaml.Interactivity;
  7. /// <summary>
  8. /// Represents a collection of <see cref="IBehavior"/>'s with a shared <see cref="AssociatedObject"/>.
  9. /// </summary>
  10. public class BehaviorCollection : AvaloniaList<AvaloniaObject>
  11. {
  12. // After a VectorChanged event we need to compare the current state of the collection
  13. // with the old collection so that we can call Detach on all removed items.
  14. private readonly List<IBehavior> _oldCollection = [];
  15. /// <summary>
  16. /// Initializes a new instance of the <see cref="BehaviorCollection"/> class.
  17. /// </summary>
  18. public BehaviorCollection()
  19. {
  20. CollectionChanged += BehaviorCollection_CollectionChanged;
  21. }
  22. /// <summary>
  23. /// Gets the <see cref="AvaloniaObject"/> to which the <see cref="BehaviorCollection"/> is attached.
  24. /// </summary>
  25. public AvaloniaObject? AssociatedObject
  26. {
  27. get;
  28. private set;
  29. }
  30. /// <summary>
  31. /// Attaches the collection of behaviors to the specified <see cref="AvaloniaObject"/>.
  32. /// </summary>
  33. /// <param name="associatedObject">The <see cref="AvaloniaObject"/> to which to attach.</param>
  34. /// <exception cref="InvalidOperationException">The <see cref="BehaviorCollection"/> is already attached to a different <see cref="AvaloniaObject"/>.</exception>
  35. public void Attach(AvaloniaObject? associatedObject)
  36. {
  37. if (Equals(associatedObject, AssociatedObject))
  38. {
  39. return;
  40. }
  41. if (AssociatedObject is not null)
  42. {
  43. throw new InvalidOperationException(
  44. "An instance of a behavior cannot be attached to more than one object at a time.");
  45. }
  46. Debug.Assert(associatedObject is not null, "The previous checks should keep us from ever setting null here.");
  47. AssociatedObject = associatedObject;
  48. foreach (var item in this)
  49. {
  50. if (item is IBehavior behavior)
  51. {
  52. behavior.Attach(AssociatedObject);
  53. }
  54. }
  55. }
  56. /// <summary>
  57. /// Detaches the collection of behaviors from the <see cref="BehaviorCollection.AssociatedObject"/>.
  58. /// </summary>
  59. public void Detach()
  60. {
  61. foreach (var item in this)
  62. {
  63. if (item is IBehavior { AssociatedObject: not null } behaviorItem)
  64. {
  65. behaviorItem.Detach();
  66. }
  67. }
  68. AssociatedObject = null;
  69. _oldCollection.Clear();
  70. }
  71. internal void AttachedToVisualTree()
  72. {
  73. foreach (var item in this)
  74. {
  75. if (item is IBehaviorEventsHandler behaviorEventsHandler)
  76. {
  77. behaviorEventsHandler.AttachedToVisualTreeEventHandler();
  78. }
  79. }
  80. }
  81. internal void DetachedFromVisualTree()
  82. {
  83. foreach (var item in this)
  84. {
  85. if (item is IBehaviorEventsHandler behaviorEventsHandler and IBehavior { AssociatedObject: not null })
  86. {
  87. behaviorEventsHandler.DetachedFromVisualTreeEventHandler();
  88. }
  89. }
  90. }
  91. internal void AttachedToLogicalTree()
  92. {
  93. foreach (var item in this)
  94. {
  95. if (item is IBehaviorEventsHandler behaviorEventsHandler)
  96. {
  97. behaviorEventsHandler.AttachedToLogicalTreeEventHandler();
  98. }
  99. }
  100. }
  101. internal void DetachedFromLogicalTree()
  102. {
  103. foreach (var item in this)
  104. {
  105. if (item is IBehaviorEventsHandler behaviorEventsHandler and IBehavior { AssociatedObject: not null })
  106. {
  107. behaviorEventsHandler.DetachedFromLogicalTreeEventHandler();
  108. }
  109. }
  110. }
  111. internal void Loaded()
  112. {
  113. foreach (var item in this)
  114. {
  115. if (item is IBehaviorEventsHandler behaviorEventsHandler)
  116. {
  117. behaviorEventsHandler.LoadedEventHandler();
  118. }
  119. }
  120. }
  121. internal void Unloaded()
  122. {
  123. foreach (var item in this)
  124. {
  125. if (item is IBehaviorEventsHandler behaviorEventsHandler and IBehavior { AssociatedObject: not null })
  126. {
  127. behaviorEventsHandler.UnloadedEventHandler();
  128. }
  129. }
  130. }
  131. private void BehaviorCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs eventArgs)
  132. {
  133. if (eventArgs.Action == NotifyCollectionChangedAction.Reset)
  134. {
  135. foreach (var behavior in _oldCollection)
  136. {
  137. if (behavior.AssociatedObject is not null)
  138. {
  139. behavior.Detach();
  140. }
  141. }
  142. _oldCollection.Clear();
  143. foreach (var newItem in this)
  144. {
  145. _oldCollection.Add(VerifiedAttach(newItem));
  146. }
  147. #if DEBUG
  148. VerifyOldCollectionIntegrity();
  149. #endif
  150. return;
  151. }
  152. switch (eventArgs.Action)
  153. {
  154. case NotifyCollectionChangedAction.Add:
  155. {
  156. var eventIndex = eventArgs.NewStartingIndex;
  157. var changedItem = eventArgs.NewItems?[0] as AvaloniaObject;
  158. _oldCollection.Insert(eventIndex, VerifiedAttach(changedItem));
  159. break;
  160. }
  161. case NotifyCollectionChangedAction.Replace:
  162. {
  163. var eventIndex = eventArgs.OldStartingIndex;
  164. eventIndex = eventIndex == -1 ? 0 : eventIndex;
  165. var changedItem = eventArgs.NewItems?[0] as AvaloniaObject;
  166. var oldItem = _oldCollection[eventIndex];
  167. if (oldItem.AssociatedObject is not null)
  168. {
  169. oldItem.Detach();
  170. }
  171. _oldCollection[eventIndex] = VerifiedAttach(changedItem);
  172. break;
  173. }
  174. case NotifyCollectionChangedAction.Remove:
  175. {
  176. var eventIndex = eventArgs.OldStartingIndex;
  177. var oldItem = _oldCollection[eventIndex];
  178. if (oldItem.AssociatedObject is not null)
  179. {
  180. oldItem.Detach();
  181. }
  182. _oldCollection.RemoveAt(eventIndex);
  183. break;
  184. }
  185. case NotifyCollectionChangedAction.Move:
  186. case NotifyCollectionChangedAction.Reset:
  187. default:
  188. {
  189. Debug.Assert(false, "Unsupported collection operation attempted.");
  190. break;
  191. }
  192. }
  193. #if DEBUG
  194. VerifyOldCollectionIntegrity();
  195. #endif
  196. }
  197. private IBehavior VerifiedAttach(AvaloniaObject? item)
  198. {
  199. if (item is not IBehavior behavior)
  200. {
  201. throw new InvalidOperationException(
  202. $"Only {nameof(IBehavior)} types are supported in a {nameof(BehaviorCollection)}.");
  203. }
  204. if (_oldCollection.Contains(behavior))
  205. {
  206. throw new InvalidOperationException(
  207. $"Cannot add an instance of a behavior to a {nameof(BehaviorCollection)} more than once.");
  208. }
  209. if (AssociatedObject is not null)
  210. {
  211. behavior.Attach(AssociatedObject);
  212. }
  213. return behavior;
  214. }
  215. [Conditional("DEBUG")]
  216. private void VerifyOldCollectionIntegrity()
  217. {
  218. var isValid = Count == _oldCollection.Count;
  219. if (isValid)
  220. {
  221. for (var i = 0; i < Count; i++)
  222. {
  223. if (!Equals(this[i], _oldCollection[i]))
  224. {
  225. isValid = false;
  226. break;
  227. }
  228. }
  229. }
  230. Debug.Assert(isValid, "Referential integrity of the collection has been compromised.");
  231. }
  232. }