File: VisualStateManager.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.Maui.Controls.Xaml;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateManager.xml" path="Type[@FullName='Microsoft.Maui.Controls.VisualStateManager']/Docs/*" />
	public static class VisualStateManager
	{
		public class CommonStates
		{
			public const string Normal = "Normal";
			public const string Disabled = "Disabled";
			public const string Focused = "Focused";
			public const string Selected = "Selected";
			public const string PointerOver = "PointerOver";
			internal const string Unfocused = "Unfocused";
		}
 
		/// <summary>Bindable property for attached property <c>VisualStateGroups</c>.</summary>
		public static readonly BindableProperty VisualStateGroupsProperty =
			BindableProperty.CreateAttached("VisualStateGroups", typeof(VisualStateGroupList), typeof(VisualElement),
				defaultValue: null, propertyChanged: VisualStateGroupsPropertyChanged,
				defaultValueCreator: bindable => new VisualStateGroupList(true) { VisualElement = (VisualElement)bindable });
 
		static void VisualStateGroupsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
		{
			if (oldValue is VisualStateGroupList { VisualElement: { } oldElement } oldVisualStateGroupList)
			{
				var vsgSpecificity = oldVisualStateGroupList.Specificity;
				var specificity = vsgSpecificity.CopyStyle(1, 0, 0, 0);
 
				foreach (var group in oldVisualStateGroupList)
				{
					if (group.CurrentState is { } state)
						foreach (var setter in state.Setters)
							setter.UnApply(oldElement, specificity);
				}
				oldVisualStateGroupList.VisualElement = null;
			}
 
			var visualElement = (VisualElement)bindable;
 
			if (newValue != null)
				((VisualStateGroupList)newValue).VisualElement = visualElement;
 
			visualElement.ChangeVisualState();
 
			UpdateStateTriggers(visualElement);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateManager.xml" path="//Member[@MemberName='GetVisualStateGroups']/Docs/*" />
		public static IList<VisualStateGroup> GetVisualStateGroups(VisualElement visualElement)
			=> (IList<VisualStateGroup>)visualElement.GetValue(VisualStateGroupsProperty);
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateManager.xml" path="//Member[@MemberName='SetVisualStateGroups']/Docs/*" />
		public static void SetVisualStateGroups(VisualElement visualElement, VisualStateGroupList value)
			=> visualElement.SetValue(VisualStateGroupsProperty, value);
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateManager.xml" path="//Member[@MemberName='GoToState']/Docs/*" />
		public static bool GoToState(VisualElement visualElement, string name)
		{
			var context = visualElement.GetContext(VisualStateGroupsProperty);
			if (context is null)
			{
				return false;
			}
 
			var vsgSpecificityValue = context.Values.GetSpecificityAndValue();
			var groups = (VisualStateGroupList)vsgSpecificityValue.Value;
			if (groups?.IsDefault != false)
			{
				return false;
			}
 
			var vsgSpecificity = vsgSpecificityValue.Key;
			groups.Specificity = vsgSpecificity;
 
			var specificity = vsgSpecificity.CopyStyle(1, 0, 0, 0);
 
			foreach (VisualStateGroup group in groups)
			{
				if (group.CurrentState?.Name == name)
				{
					// We're already in the target state; nothing else to do
					return true;
				}
 
				// See if this group contains the new state
				var target = group.GetState(name);
				if (target == null)
				{
					continue;
				}
 
				// If we've got a new state to transition to, unapply the setters from the current state
				if (group.CurrentState != null)
				{
					foreach (Setter setter in group.CurrentState.Setters)
					{
						setter.UnApply(visualElement, specificity);
					}
				}
 
				// Update the current state
				group.CurrentState = target;
 
				// Apply the setters from the new state
				foreach (Setter setter in target.Setters)
				{
					setter.Apply(visualElement, specificity);
				}
 
				return true;
			}
 
			return false;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateManager.xml" path="//Member[@MemberName='HasVisualStateGroups']/Docs/*" />
		public static bool HasVisualStateGroups(this VisualElement element)
		{
			if (!element.IsSet(VisualStateGroupsProperty))
				return false;
 
			if (GetVisualStateGroups(element) is VisualStateGroupList vsgl)
				return !vsgl.IsDefault;
 
			return true;
		}
 
		internal static void UpdateStateTriggers(VisualElement visualElement)
		{
			var groups = (IList<VisualStateGroup>)visualElement.GetValue(VisualStateGroupsProperty);
 
			foreach (VisualStateGroup group in groups)
			{
				group.VisualElement = visualElement;
				group.UpdateStateTriggers();
			}
		}
	}
 
	/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="Type[@FullName='Microsoft.Maui.Controls.VisualStateGroupList']/Docs/*" />
	public class VisualStateGroupList : IList<VisualStateGroup>
	{
		readonly IList<VisualStateGroup> _internalList;
		internal bool IsDefault { get; private set; }
 
		// Used to check for duplicate names; we keep it around because it's cheaper to create it once and clear it
		// than to create one every time we need to validate
		readonly HashSet<string> _names = new HashSet<string>(StringComparer.Ordinal);
 
		void Validate(IList<VisualStateGroup> groups)
		{
			var groupCount = groups.Count;
 
			// If we only have 1 group, no need to worry about duplicate group names
			if (groupCount > 1)
			{
				_names.Clear();
 
				// Using a for loop to avoid allocating an enumerator
				for (int n = 0; n < groupCount; n++)
				{
					// HashSet will return false if the string is already in the set
					if (!_names.Add(groups[n].Name))
					{
						throw new InvalidOperationException("VisualStateGroup Names must be unique");
					}
				}
			}
 
			// State names must be unique within this group list, so we'll iterate over all the groups
			// and their states and add the state names to a HashSet; we throw an exception if a duplicate shows up
 
			_names.Clear();
 
			// Using nested for loops to avoid allocating enumerators
			for (int groupIndex = 0; groupIndex < groupCount; groupIndex++)
			{
				// Cache the group lookup and states count; it's ugly, but it speeds things up a lot
				var group = groups[groupIndex];
				group.VisualElement = VisualElement;
				group.UpdateStateTriggers();
 
				var stateCount = group.States.Count;
 
				for (int stateIndex = 0; stateIndex < stateCount; stateIndex++)
				{
					// HashSet will return false if the string is already in the set
					if (!_names.Add(group.States[stateIndex].Name))
					{
						throw new InvalidOperationException("VisualState Names must be unique");
					}
				}
			}
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='.ctor'][1]/Docs/*" />
		public VisualStateGroupList() : this(false)
		{
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='.ctor'][2]/Docs/*" />
		public VisualStateGroupList(bool isDefault)
		{
			IsDefault = isDefault;
			_internalList = new WatchAddList<VisualStateGroup>(ValidateAndNotify);
		}
 
		void ValidateAndNotify(object sender, EventArgs eventArgs)
		{
			ValidateAndNotify(_internalList);
		}
 
		void ValidateAndNotify(IList<VisualStateGroup> groups)
		{
			if (groups.Count > 0)
				IsDefault = false;
 
			Validate(groups);
			OnStatesChanged();
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='GetEnumerator']/Docs/*" />
		public IEnumerator<VisualStateGroup> GetEnumerator()
		{
			return _internalList.GetEnumerator();
		}
 
		IEnumerator IEnumerable.GetEnumerator()
		{
			return ((IEnumerable)_internalList).GetEnumerator();
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='Add']/Docs/*" />
		public void Add(VisualStateGroup item)
		{
			if (item == null)
			{
				throw new ArgumentNullException(nameof(item));
			}
 
			_internalList.Add(item);
 
			item.StatesChanged += ValidateAndNotify;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='Clear']/Docs/*" />
		public void Clear()
		{
			foreach (var group in _internalList)
			{
				group.StatesChanged -= ValidateAndNotify;
			}
 
			_internalList.Clear();
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='Contains']/Docs/*" />
		public bool Contains(VisualStateGroup item)
		{
			return _internalList.Contains(item);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='CopyTo']/Docs/*" />
		public void CopyTo(VisualStateGroup[] array, int arrayIndex)
		{
			_internalList.CopyTo(array, arrayIndex);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='Remove']/Docs/*" />
		public bool Remove(VisualStateGroup item)
		{
			if (item == null)
			{
				throw new ArgumentNullException(nameof(item));
			}
 
			item.StatesChanged -= ValidateAndNotify;
			return _internalList.Remove(item);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='Count']/Docs/*" />
		public int Count => _internalList.Count;
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='IsReadOnly']/Docs/*" />
		public bool IsReadOnly => false;
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='IndexOf']/Docs/*" />
		public int IndexOf(VisualStateGroup item)
		{
			return _internalList.IndexOf(item);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='Insert']/Docs/*" />
		public void Insert(int index, VisualStateGroup item)
		{
			if (item == null)
			{
				throw new ArgumentNullException(nameof(item));
			}
 
			item.StatesChanged += ValidateAndNotify;
			_internalList.Insert(index, item);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroupList.xml" path="//Member[@MemberName='RemoveAt']/Docs/*" />
		public void RemoveAt(int index)
		{
			_internalList[index].StatesChanged -= ValidateAndNotify;
			_internalList.RemoveAt(index);
		}
 
		public VisualStateGroup this[int index]
		{
			get => _internalList[index];
			set => _internalList[index] = value;
		}
 
		WeakReference<VisualElement> _visualElement;
		internal VisualElement VisualElement
		{
			get
			{
				if (_visualElement == null)
					return null;
				_visualElement.TryGetTarget(out var ve);
				return ve;
			}
			set
			{
				_visualElement = new WeakReference<VisualElement>(value);
			}
		}
 
		internal SetterSpecificity Specificity { get; set; }
 
		void OnStatesChanged()
		{
			VisualElement?.ChangeVisualState();
		}
 
		public override bool Equals(object obj) => Equals(obj as VisualStateGroupList);
		bool Equals(VisualStateGroupList other)
		{
			if (other is null)
				return false;
			if (Object.ReferenceEquals(this, other))
				return true;
			if (Count != other.Count)
				return false;
			for (var i = 0; i < Count; i++)
				if (!this[i].Equals(other[i]))
					return false;
			return true;
		}
 
		public override int GetHashCode()
		{
			unchecked
			{
				var hash = 41;
				for (var i = 0; i < Count; i++)
					hash = (hash * 43) ^ this[i].GetHashCode();
				return hash;
			}
		}
 
	}
 
	/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroup.xml" path="Type[@FullName='Microsoft.Maui.Controls.VisualStateGroup']/Docs/*" />
	[RuntimeNameProperty(nameof(Name))]
	[ContentProperty(nameof(States))]
	public sealed class VisualStateGroup
	{
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroup.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public VisualStateGroup()
		{
			States = new WatchAddList<VisualState>(OnStatesChanged);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroup.xml" path="//Member[@MemberName='TargetType']/Docs/*" />
		public Type TargetType { get; set; }
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroup.xml" path="//Member[@MemberName='Name']/Docs/*" />
		public string Name { get; set; }
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroup.xml" path="//Member[@MemberName='States']/Docs/*" />
		public IList<VisualState> States { get; }
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualStateGroup.xml" path="//Member[@MemberName='CurrentState']/Docs/*" />
		public VisualState CurrentState { get; internal set; }
 
		WeakReference<VisualElement> _visualElement;
		internal VisualElement VisualElement
		{
			get
			{
				if (_visualElement == null)
					return null;
				_visualElement.TryGetTarget(out var ve);
				return ve;
			}
			set
			{
				_visualElement = new WeakReference<VisualElement>(value);
			}
		}
 
		internal VisualState GetState(string name)
		{
			foreach (VisualState state in States)
			{
				if (string.Equals(state.Name, name, StringComparison.Ordinal))
				{
					return state;
				}
			}
 
			return null;
		}
 
		internal bool HasStateTriggers()
		{
			bool hasStateTriggers = false;
 
			foreach (VisualState state in States)
			{
				if (state.StateTriggers.Count > 0)
				{
					hasStateTriggers = true;
					break;
				}
			}
 
			return hasStateTriggers;
		}
 
		internal VisualState GetActiveTrigger()
		{
			var defaultState = default(VisualState);
			var visualState = defaultState;
			var conflicts = new List<StateTriggerBase>();
 
			for (var stateIndex = 0; stateIndex < States.Count; stateIndex++)
			{
				var state = States[stateIndex];
				for (var triggerIndex = 0; triggerIndex < state.StateTriggers.Count; triggerIndex++)
				{
					var trigger = state.StateTriggers[triggerIndex];
 
					if (trigger.IsActive)
					{
						if (visualState == defaultState)
							visualState = state;
 
						conflicts.Add(trigger);
					}
				}
			}
 
			if (conflicts.Count > 1)
				visualState = ResolveStateTriggersConflict(conflicts);
 
			return visualState;
		}
 
		VisualState ResolveStateTriggersConflict(List<StateTriggerBase> conflicts)
		{
			// When using StateTriggers to control visual states, the trigger engine uses the following rules to 
			// score triggers and determine which trigger, and the corresponding VisualState, will be active:
			//
			// 1. Custom trigger that derives from StateTriggerBase
			// 2. AdaptiveTrigger activated due to MinWindowWidth
			// 3. AdaptiveTrigger activated due to MinWindowHeight
			//
			// If there are multiple active triggers at a time that have a conflict in scoring (i.e.two custom 
			// triggers), then the first one declared in the markup file takes precedence.
 
			var existCustomTriggers = conflicts.Where(c => !(c is AdaptiveTrigger));
 
			if (existCustomTriggers.Count() > 1)
			{
				var firstExistCustomTrigger = existCustomTriggers.FirstOrDefault();
				return firstExistCustomTrigger.VisualState;
			}
 
			var adaptiveTriggers = conflicts.Where(c => c is AdaptiveTrigger);
 
			var minWindowWidthAdaptiveTriggers = adaptiveTriggers.Where(c => ((AdaptiveTrigger)c).MinWindowWidth != -1d).OrderByDescending(c => ((AdaptiveTrigger)c).MinWindowWidth);
			var latestMinWindowWidthAdaptiveTrigger = minWindowWidthAdaptiveTriggers.FirstOrDefault();
 
			if (latestMinWindowWidthAdaptiveTrigger != null)
				return latestMinWindowWidthAdaptiveTrigger.VisualState;
 
			var minWindowHeightAdaptiveTriggers = adaptiveTriggers.Where(c => ((AdaptiveTrigger)c).MinWindowHeight != -1d).OrderByDescending(c => ((AdaptiveTrigger)c).MinWindowHeight);
			var latestMinWindowHeightAdaptiveTrigger = minWindowHeightAdaptiveTriggers.FirstOrDefault();
 
			if (latestMinWindowHeightAdaptiveTrigger != null)
				return latestMinWindowHeightAdaptiveTrigger.VisualState;
 
			return default;
		}
 
		internal VisualStateGroup Clone()
		{
			var clone = new VisualStateGroup { TargetType = TargetType, Name = Name, CurrentState = CurrentState, VisualElement = VisualElement };
 
			foreach (VisualState state in States)
			{
				state.VisualStateGroup = clone;
				clone.States.Add(state.Clone());
			}
 
			if (VisualDiagnostics.IsEnabled && VisualDiagnostics.GetSourceInfo(this) is SourceInfo info)
				VisualDiagnostics.RegisterSourceInfo(clone, info.SourceUri, info.LineNumber, info.LinePosition);
 
			return clone;
		}
 
		internal void UpdateStateTriggers()
		{
			if (VisualElement == null)
				return;
 
			bool hasStateTriggers = HasStateTriggers();
 
			if (!hasStateTriggers)
				return;
 
			var newStateTrigger = GetActiveTrigger();
 
			if (newStateTrigger == null)
				return;
 
			var oldStateTrigger = CurrentState;
 
			if (newStateTrigger == oldStateTrigger)
				return;
 
			VisualStateManager.GoToState(VisualElement, newStateTrigger.Name);
		}
 
		internal event EventHandler StatesChanged;
 
		void OnStatesChanged(IList<VisualState> states)
		{
			if (states.Any(state => string.IsNullOrEmpty(state.Name)))
			{
				throw new InvalidOperationException("State names may not be null or empty");
			}
 
			foreach (var state in states)
			{
				state.VisualStateGroup = this;
			}
 
			StatesChanged?.Invoke(this, EventArgs.Empty);
		}
 
		public override bool Equals(object obj) => Equals(obj as VisualStateGroup);
 
		bool Equals(VisualStateGroup other)
		{
			if (other is null)
				return false;
			if (object.ReferenceEquals(this, other))
				return true;
			if (Name != other.Name)
				return false;
			if (TargetType != other.TargetType)
				return false;
			if (States.Count != other.States.Count)
				return false;
			for (var i = 0; i < States.Count; i++)
				if (!States[i].Equals(other.States[i]))
					return false;
			return true;
		}
 
		public override int GetHashCode()
		{
			unchecked
			{
				var hash = (Name, TargetType).GetHashCode();
				for (var i = 0; i < States.Count; i++)
					hash = (hash * 43) ^ States[i].GetHashCode();
				return hash;
			}
		}
	}
 
	/// <include file="../../docs/Microsoft.Maui.Controls/VisualState.xml" path="Type[@FullName='Microsoft.Maui.Controls.VisualState']/Docs/*" />
	[RuntimeNameProperty(nameof(Name))]
	public sealed class VisualState
	{
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualState.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public VisualState()
		{
			Setters = new ObservableCollection<Setter>();
			StateTriggers = new WatchAddList<StateTriggerBase>(OnStateTriggersChanged);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualState.xml" path="//Member[@MemberName='Name']/Docs/*" />
		public string Name { get; set; }
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualState.xml" path="//Member[@MemberName='Setters']/Docs/*" />
		public IList<Setter> Setters { get; }
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualState.xml" path="//Member[@MemberName='StateTriggers']/Docs/*" />
		public IList<StateTriggerBase> StateTriggers { get; }
		/// <include file="../../docs/Microsoft.Maui.Controls/VisualState.xml" path="//Member[@MemberName='TargetType']/Docs/*" />
		public Type TargetType { get; set; }
		internal VisualStateGroup VisualStateGroup { get; set; }
 
		internal VisualState Clone()
		{
			var clone = new VisualState { Name = Name, TargetType = TargetType };
 
			foreach (var setter in Setters)
			{
				clone.Setters.Add(setter);
			}
 
			foreach (var stateTrigger in StateTriggers)
			{
				stateTrigger.VisualState = this;
				clone.StateTriggers.Add(stateTrigger);
			}
 
			if (VisualDiagnostics.IsEnabled && VisualDiagnostics.GetSourceInfo(this) is SourceInfo info)
				VisualDiagnostics.RegisterSourceInfo(clone, info.SourceUri, info.LineNumber, info.LinePosition);
 
			return clone;
		}
 
		void OnStateTriggersChanged(IList<StateTriggerBase> stateTriggers)
		{
			foreach (var stateTrigger in stateTriggers)
			{
				stateTrigger.VisualState = this;
			}
 
			VisualStateGroup?.UpdateStateTriggers();
		}
 
		public override bool Equals(object obj) => Equals(obj as VisualState);
 
		bool Equals(VisualState other)
		{
			if (other is null)
				return false;
			if (object.ReferenceEquals(this, other))
				return true;
			if (Name != other.Name)
				return false;
			if (TargetType != other.TargetType)
				return false;
			if (Setters.Count != other.Setters.Count)
				return false;
			if (StateTriggers.Count != other.StateTriggers.Count)
				return false;
			for (var i = 0; i < Setters.Count; i++)
				if (!Setters[i].Equals(other.Setters[i]))
					return false;
			for (var i = 0; i < StateTriggers.Count; i++)
				if (!StateTriggers[i].Equals(other.StateTriggers[i]))
					return false;
			return true;
		}
 
		public override int GetHashCode()
		{
			unchecked
			{
				var hash = (Name, TargetType).GetHashCode();
				for (var i = 0; i < Setters.Count; i++)
					hash = (hash * 43) ^ Setters[i].GetHashCode();
				for (var i = 0; i < StateTriggers.Count; i++)
					hash = (hash * 43) ^ StateTriggers[i].GetHashCode();
				return hash;
			}
		}
	}
 
	internal static class VisualStateGroupListExtensions
	{
		internal static IList<VisualStateGroup> Clone(this IList<VisualStateGroup> groups)
		{
			var clone = new VisualStateGroupList();
 
			foreach (var group in groups)
			{
				group.VisualElement = clone.VisualElement;
				clone.Add(group.Clone());
			}
 
			if (VisualDiagnostics.IsEnabled && VisualDiagnostics.GetSourceInfo(groups) is SourceInfo info)
				VisualDiagnostics.RegisterSourceInfo(clone, info.SourceUri, info.LineNumber, info.LinePosition);
 
			return clone;
		}
 
		internal static bool HasVisualState(this VisualElement element, string name)
		{
			IList<VisualStateGroup> list = VisualStateManager.GetVisualStateGroups(element);
			for (var i = 0; i < list.Count; i++)
			{
				VisualStateGroup group = list[i];
				for (var j = 0; j < group.States.Count; j++)
				{
					if (group.States[j].Name == name)
						return true;
				}
			}
 
			return false;
		}
	}
 
	internal class WatchAddList<T> : IList<T>
	{
		readonly Action<List<T>> _onAdd;
		readonly List<T> _internalList;
 
		public WatchAddList(Action<List<T>> onAdd)
		{
			_onAdd = onAdd;
			_internalList = new List<T>();
		}
 
		public IEnumerator<T> GetEnumerator()
		{
			return _internalList.GetEnumerator();
		}
 
		IEnumerator IEnumerable.GetEnumerator()
		{
			return ((IEnumerable)_internalList).GetEnumerator();
		}
 
		public void Add(T item)
		{
			_internalList.Add(item);
			_onAdd(_internalList);
		}
 
		public void Clear()
		{
			_internalList.Clear();
		}
 
		public bool Contains(T item)
		{
			return _internalList.Contains(item);
		}
 
		public void CopyTo(T[] array, int arrayIndex)
		{
			_internalList.CopyTo(array, arrayIndex);
		}
 
		public bool Remove(T item)
		{
			return _internalList.Remove(item);
		}
 
		public int Count => _internalList.Count;
 
		public bool IsReadOnly => false;
 
		public int IndexOf(T item)
		{
			return _internalList.IndexOf(item);
		}
 
		public void Insert(int index, T item)
		{
			_internalList.Insert(index, item);
			_onAdd(_internalList);
		}
 
		public void RemoveAt(int index)
		{
			_internalList.RemoveAt(index);
		}
 
		public T this[int index]
		{
			get => _internalList[index];
			set => _internalList[index] = value;
		}
	}
}