using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using Avalonia.Collections; namespace Avalonia.Xaml.Interactivity; /// /// Represents a collection of 's with a shared . /// public class BehaviorCollection : AvaloniaList { // After a VectorChanged event we need to compare the current state of the collection // with the old collection so that we can call Detach on all removed items. private readonly List _oldCollection = []; /// /// Initializes a new instance of the class. /// public BehaviorCollection() { CollectionChanged += BehaviorCollection_CollectionChanged; } /// /// Gets the to which the is attached. /// public AvaloniaObject? AssociatedObject { get; private set; } /// /// Attaches the collection of behaviors to the specified . /// /// The to which to attach. /// The is already attached to a different . public void Attach(AvaloniaObject? associatedObject) { if (Equals(associatedObject, AssociatedObject)) { return; } if (AssociatedObject is not null) { throw new InvalidOperationException( "An instance of a behavior cannot be attached to more than one object at a time."); } Debug.Assert(associatedObject is not null, "The previous checks should keep us from ever setting null here."); AssociatedObject = associatedObject; foreach (var item in this) { if (item is IBehavior behavior) { behavior.Attach(AssociatedObject); } } } /// /// Detaches the collection of behaviors from the . /// public void Detach() { foreach (var item in this) { if (item is IBehavior { AssociatedObject: not null } behaviorItem) { behaviorItem.Detach(); } } AssociatedObject = null; _oldCollection.Clear(); } internal void AttachedToVisualTree() { foreach (var item in this) { if (item is IBehaviorEventsHandler behaviorEventsHandler) { behaviorEventsHandler.AttachedToVisualTreeEventHandler(); } } } internal void DetachedFromVisualTree() { foreach (var item in this) { if (item is IBehaviorEventsHandler behaviorEventsHandler and IBehavior { AssociatedObject: not null }) { behaviorEventsHandler.DetachedFromVisualTreeEventHandler(); } } } internal void AttachedToLogicalTree() { foreach (var item in this) { if (item is IBehaviorEventsHandler behaviorEventsHandler) { behaviorEventsHandler.AttachedToLogicalTreeEventHandler(); } } } internal void DetachedFromLogicalTree() { foreach (var item in this) { if (item is IBehaviorEventsHandler behaviorEventsHandler and IBehavior { AssociatedObject: not null }) { behaviorEventsHandler.DetachedFromLogicalTreeEventHandler(); } } } internal void Loaded() { foreach (var item in this) { if (item is IBehaviorEventsHandler behaviorEventsHandler) { behaviorEventsHandler.LoadedEventHandler(); } } } internal void Unloaded() { foreach (var item in this) { if (item is IBehaviorEventsHandler behaviorEventsHandler and IBehavior { AssociatedObject: not null }) { behaviorEventsHandler.UnloadedEventHandler(); } } } private void BehaviorCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs eventArgs) { if (eventArgs.Action == NotifyCollectionChangedAction.Reset) { foreach (var behavior in _oldCollection) { if (behavior.AssociatedObject is not null) { behavior.Detach(); } } _oldCollection.Clear(); foreach (var newItem in this) { _oldCollection.Add(VerifiedAttach(newItem)); } #if DEBUG VerifyOldCollectionIntegrity(); #endif return; } switch (eventArgs.Action) { case NotifyCollectionChangedAction.Add: { var eventIndex = eventArgs.NewStartingIndex; var changedItem = eventArgs.NewItems?[0] as AvaloniaObject; _oldCollection.Insert(eventIndex, VerifiedAttach(changedItem)); break; } case NotifyCollectionChangedAction.Replace: { var eventIndex = eventArgs.OldStartingIndex; eventIndex = eventIndex == -1 ? 0 : eventIndex; var changedItem = eventArgs.NewItems?[0] as AvaloniaObject; var oldItem = _oldCollection[eventIndex]; if (oldItem.AssociatedObject is not null) { oldItem.Detach(); } _oldCollection[eventIndex] = VerifiedAttach(changedItem); break; } case NotifyCollectionChangedAction.Remove: { var eventIndex = eventArgs.OldStartingIndex; var oldItem = _oldCollection[eventIndex]; if (oldItem.AssociatedObject is not null) { oldItem.Detach(); } _oldCollection.RemoveAt(eventIndex); break; } case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Reset: default: { Debug.Assert(false, "Unsupported collection operation attempted."); break; } } #if DEBUG VerifyOldCollectionIntegrity(); #endif } private IBehavior VerifiedAttach(AvaloniaObject? item) { if (item is not IBehavior behavior) { throw new InvalidOperationException( $"Only {nameof(IBehavior)} types are supported in a {nameof(BehaviorCollection)}."); } if (_oldCollection.Contains(behavior)) { throw new InvalidOperationException( $"Cannot add an instance of a behavior to a {nameof(BehaviorCollection)} more than once."); } if (AssociatedObject is not null) { behavior.Attach(AssociatedObject); } return behavior; } [Conditional("DEBUG")] private void VerifyOldCollectionIntegrity() { var isValid = Count == _oldCollection.Count; if (isValid) { for (var i = 0; i < Count; i++) { if (!Equals(this[i], _oldCollection[i])) { isValid = false; break; } } } Debug.Assert(isValid, "Referential integrity of the collection has been compromised."); } }