File: TemplatedItemsList.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.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Cadenza.Collections;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Devices;
 
namespace Microsoft.Maui.Controls.Internals
{
 
	[EditorBrowsable(EditorBrowsableState.Never)]
	public sealed class TemplatedItemsList<TView, [DynamicallyAccessedMembers(BindableProperty.DeclaringTypeMembers | BindableProperty.ReturnTypeMembers)] TItem> : BindableObject, ITemplatedItemsList<TItem>, IList, IDisposable
												where TView : BindableObject, IItemsView<TItem>
												where TItem : BindableObject
	{
		/// <summary>Bindable property for <see cref="Name"/>.</summary>
		public static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(TemplatedItemsList<TView, TItem>), null);
 
		/// <summary>Bindable property for <see cref="ShortName"/>.</summary>
		public static readonly BindableProperty ShortNameProperty = BindableProperty.Create(nameof(ShortName), typeof(string), typeof(TemplatedItemsList<TView, TItem>), null);
 
		static readonly BindablePropertyKey HeaderContentPropertyKey = BindableProperty.CreateReadOnly(nameof(HeaderContent), typeof(TItem), typeof(TemplatedItemsList<TView, TItem>), null);
 
		internal static readonly BindablePropertyKey ListProxyPropertyKey = BindableProperty.CreateReadOnly(nameof(ListProxy), typeof(ListProxy), typeof(TemplatedItemsList<TView, TItem>), null,
			propertyChanged: OnListProxyChanged);
 
		static readonly BindableProperty GroupProperty = BindableProperty.Create("Group", typeof(TemplatedItemsList<TView, TItem>), typeof(TItem), null);
 
		static readonly BindableProperty IndexProperty = BindableProperty.Create("Index", typeof(int), typeof(TItem), -1);
 
		static readonly BindablePropertyKey IsGroupHeaderPropertyKey = BindableProperty.CreateAttachedReadOnly("IsGroupHeader", typeof(bool), typeof(Cell), false);
 
		readonly BindableProperty _itemSourceProperty;
		readonly BindableProperty _itemTemplateProperty;
 
		readonly TView _itemsView;
 
		readonly List<TItem> _templatedObjects = new List<TItem>();
 
		bool _disposed;
		BindingBase _groupDisplayBinding;
		Cadenza.Collections.OrderedDictionary<object, TemplatedItemsList<TView, TItem>> _groupedItems;
		DataTemplate _groupHeaderTemplate;
		BindingBase _groupShortNameBinding;
		ShortNamesProxy _shortNames;
 
		internal TemplatedItemsList(TView itemsView, BindableProperty itemSourceProperty, BindableProperty itemTemplateProperty)
		{
			if (itemsView == null)
				throw new ArgumentNullException(nameof(itemsView));
			if (itemSourceProperty == null)
				throw new ArgumentNullException(nameof(itemSourceProperty));
			if (itemTemplateProperty == null)
				throw new ArgumentNullException(nameof(itemTemplateProperty));
 
			_itemsView = itemsView;
			_itemsView.PropertyChanged += BindableOnPropertyChanged;
 
			_itemSourceProperty = itemSourceProperty;
			_itemTemplateProperty = itemTemplateProperty;
 
			IEnumerable source = GetItemsViewSource();
			if (source != null)
				ListProxy = new ListProxy(source, dispatcher: _itemsView.Dispatcher);
			else
				ListProxy = new ListProxy(Array.Empty<object>(), dispatcher: _itemsView.Dispatcher);
		}
 
		internal TemplatedItemsList(TemplatedItemsList<TView, TItem> parent, IEnumerable itemSource, TView itemsView, BindableProperty itemTemplateProperty, int windowSize = int.MaxValue)
		{
			if (itemsView == null)
				throw new ArgumentNullException(nameof(itemsView));
			if (itemTemplateProperty == null)
				throw new ArgumentNullException(nameof(itemTemplateProperty));
 
			Parent = parent;
 
			_itemsView = itemsView;
			_itemsView.PropertyChanged += BindableOnPropertyChanged;
			_itemTemplateProperty = itemTemplateProperty;
 
			if (itemSource != null)
			{
				ListProxy = new ListProxy(itemSource, windowSize, _itemsView.Dispatcher);
				ListProxy.CollectionChanged += OnProxyCollectionChanged;
			}
			else
				ListProxy = new ListProxy(Array.Empty<object>(), dispatcher: _itemsView.Dispatcher);
		}
 
		event PropertyChangedEventHandler ITemplatedItemsList<TItem>.PropertyChanged
		{
			add { PropertyChanged += value; }
			remove { PropertyChanged -= value; }
		}
 
		[DoesNotInheritDataType]
		public BindingBase GroupDisplayBinding
		{
			get { return _groupDisplayBinding; }
			set
			{
				_groupDisplayBinding = value;
				OnHeaderTemplateChanged();
			}
		}
 
		public DataTemplate GroupHeaderTemplate
		{
			get
			{
				DataTemplate groupHeader = null;
				if (GroupHeaderTemplateProperty != null)
					groupHeader = (DataTemplate)_itemsView.GetValue(GroupHeaderTemplateProperty);
 
				return groupHeader ?? _groupHeaderTemplate;
			}
 
			set
			{
				if (_groupHeaderTemplate == value)
					return;
 
				_groupHeaderTemplate = value;
				OnHeaderTemplateChanged();
			}
		}
 
		public BindableProperty GroupHeaderTemplateProperty { get; set; }
 
		[DoesNotInheritDataType]
		public BindingBase GroupShortNameBinding
		{
			get { return _groupShortNameBinding; }
			set
			{
				_groupShortNameBinding = value;
				OnShortNameBindingChanged();
			}
		}
 
		public TItem HeaderContent
		{
			get { return (TItem)GetValue(HeaderContentPropertyKey.BindableProperty); }
			private set { SetValue(HeaderContentPropertyKey, value); }
		}
 
		public bool IsGroupingEnabled
		{
			get { return (IsGroupingEnabledProperty != null) && (bool)_itemsView.GetValue(IsGroupingEnabledProperty); }
		}
 
		public BindableProperty IsGroupingEnabledProperty { get; set; }
 
		public IEnumerable ItemsSource
		{
			get { return ListProxy.ProxiedEnumerable; }
		}
 
		public string Name
		{
			get { return (string)GetValue(NameProperty); }
			set { SetValue(NameProperty, value); }
		}
 
		public TemplatedItemsList<TView, TItem> Parent { get; }
 
		public BindableProperty ProgressiveLoadingProperty { get; set; }
 
		public string ShortName
		{
			get { return (string)GetValue(ShortNameProperty); }
			set { SetValue(ShortNameProperty, value); }
		}
 
		public IReadOnlyList<string> ShortNames
		{
			get { return _shortNames; }
		}
 
		internal ListViewCachingStrategy CachingStrategy
		{
			get
			{
				var listView = _itemsView as ListView;
				if (listView == null)
					return ListViewCachingStrategy.RetainElement;
 
				return listView.CachingStrategy;
			}
		}
 
		internal IListProxy ListProxy
		{
			get { return (IListProxy)GetValue(ListProxyPropertyKey.BindableProperty); }
			private set { SetValue(ListProxyPropertyKey, value); }
		}
 
		IListProxy ITemplatedItemsList<TItem>.ListProxy
		{
			get { return ListProxy; }
		}
 
		DataTemplate ItemTemplate
		{
			get { return (DataTemplate)_itemsView.GetValue(_itemTemplateProperty); }
		}
 
		bool ProgressiveLoading
		{
			get { return (ProgressiveLoadingProperty != null) && (bool)_itemsView.GetValue(ProgressiveLoadingProperty); }
		}
 
		void ICollection.CopyTo(Array array, int arrayIndex)
		{
			throw new NotImplementedException();
		}
 
		bool ICollection.IsSynchronized
		{
			get { return false; }
		}
 
		object ICollection.SyncRoot
		{
			get { return this; }
		}
 
		public void Dispose()
		{
			if (_disposed)
				return;
 
			_itemsView.PropertyChanged -= BindableOnPropertyChanged;
 
			TItem header = HeaderContent;
			if (header != null)
				UnhookItem(header);
 
			for (var i = 0; i < _templatedObjects.Count; i++)
			{
				TItem item = _templatedObjects[i];
				if (item != null)
					UnhookItem(item);
			}
 
			_disposed = true;
		}
 
		IEnumerator IEnumerable.GetEnumerator()
		{
			if (IsGroupingEnabled)
				return _groupedItems.Values.GetEnumerator();
 
			return GetEnumerator();
		}
 
		public IEnumerator<TItem> GetEnumerator()
		{
			var i = 0;
			foreach (object item in ListProxy)
				yield return GetOrCreateContent(i++, item);
		}
 
		int IList.Add(object item)
		{
			throw new NotSupportedException();
		}
 
		void IList.Clear()
		{
			throw new NotSupportedException();
		}
 
		bool IList.Contains(object item)
		{
			throw new NotImplementedException();
		}
 
		int IList.IndexOf(object item)
		{
			if (IsGroupingEnabled)
			{
				var til = item as TemplatedItemsList<TView, TItem>;
				if (til != null)
					return _groupedItems.Values.IndexOf(til);
			}
 
			return IndexOf((TItem)item);
		}
 
		void IList.Insert(int index, object item)
		{
			throw new NotSupportedException();
		}
 
		bool IList.IsFixedSize
		{
			get { return false; }
		}
 
		bool IList.IsReadOnly
		{
			get { return true; }
		}
 
		object IList.this[int index]
		{
			get
			{
				if (IsGroupingEnabled)
					return GetGroup(index);
 
				return this[index];
			}
			set { throw new NotSupportedException(); }
		}
 
		void IList.Remove(object item)
		{
			throw new NotSupportedException();
		}
 
		void IList.RemoveAt(int index)
		{
			throw new NotSupportedException();
		}
 
		public event NotifyCollectionChangedEventHandler CollectionChanged;
 
		public int Count
		{
			get { return ListProxy.Count; }
		}
 
		public TItem this[int index]
		{
			get { return GetOrCreateContent(index, ListProxy[index]); }
		}
 
		public int GetDescendantCount()
		{
			if (!IsGroupingEnabled)
				return Count;
 
			if (_groupedItems == null)
				return 0;
 
			int count = Count;
			foreach (TemplatedItemsList<TView, TItem> group in _groupedItems.Values)
				count += group.GetDescendantCount();
 
			return count;
		}
 
		public int GetGlobalIndexForGroup(ITemplatedItemsList<TItem> group)
		{
			if (group == null)
				throw new ArgumentNullException(nameof(group));
 
			int groupIndex = _groupedItems.Values.IndexOf(group);
 
			var index = 0;
			for (var i = 0; i < groupIndex; i++)
				index += _groupedItems[i].GetDescendantCount() + 1;
 
			return index;
		}
 
		public int GetGlobalIndexOfGroup(object item)
		{
			var count = 0;
			if (IsGroupingEnabled && _groupedItems != null)
			{
				foreach (object group in _groupedItems.Keys)
				{
					if (group == item)
						return count;
					count++;
				}
			}
 
			return -1;
		}
 
		public int GetGlobalIndexOfItem(object item)
		{
			if (!IsGroupingEnabled)
				return ListProxy.IndexOf(item);
 
			var count = 0;
			if (_groupedItems != null)
			{
				foreach (TemplatedItemsList<TView, TItem> children in _groupedItems.Values)
				{
					count++;
 
					int index = children.GetGlobalIndexOfItem(item);
					if (index != -1)
						return count + index;
 
					count += children.GetDescendantCount();
				}
			}
 
			return -1;
		}
 
		public int GetGlobalIndexOfItem(object group, object item)
		{
			if (!IsGroupingEnabled)
				return ListProxy.IndexOf(item);
 
			var count = 0;
			if (_groupedItems != null)
			{
				foreach (KeyValuePair<object, TemplatedItemsList<TView, TItem>> kvp in _groupedItems)
				{
					count++;
 
					if (ReferenceEquals(group, kvp.Key))
					{
						int index = kvp.Value.GetGlobalIndexOfItem(item);
						if (index != -1)
							return count + index;
					}
 
					count += kvp.Value.GetDescendantCount();
				}
			}
 
			return -1;
		}
 
		public Tuple<int, int> GetGroupAndIndexOfItem(object item)
		{
			if (item == null)
				return new Tuple<int, int>(-1, -1);
			if (!IsGroupingEnabled)
				return new Tuple<int, int>(0, GetGlobalIndexOfItem(item));
 
			var group = 0;
			if (_groupedItems != null)
			{
				foreach (TemplatedItemsList<TView, TItem> children in _groupedItems.Values)
				{
					int index = children.GetGlobalIndexOfItem(item);
					if (index != -1)
						return new Tuple<int, int>(group, index);
 
					group++;
				}
			}
 
			return new Tuple<int, int>(-1, -1);
		}
 
		public Tuple<int, int> GetGroupAndIndexOfItem(object group, object item)
		{
			if (!IsGroupingEnabled)
				return new Tuple<int, int>(0, GetGlobalIndexOfItem(item));
			if (_groupedItems == null)
				return new Tuple<int, int>(-1, -1);
 
			var groupIndex = 0;
			foreach (TemplatedItemsList<TView, TItem> children in _groupedItems.Values)
			{
				if (ReferenceEquals(children.BindingContext, group) || group == null)
				{
					for (var i = 0; i < children.Count; i++)
					{
						if (ReferenceEquals(children[i].BindingContext, item))
							return new Tuple<int, int>(groupIndex, i);
					}
 
					if (group != null)
						return new Tuple<int, int>(groupIndex, -1);
				}
 
				groupIndex++;
			}
 
			return new Tuple<int, int>(-1, -1);
		}
 
		public int GetGroupIndexFromGlobal(int globalIndex, out int leftOver)
		{
			leftOver = 0;
 
			var index = 0;
			for (var i = 0; i < _groupedItems.Count; i++)
			{
				if (index == globalIndex)
					return i;
 
				TemplatedItemsList<TView, TItem> group = _groupedItems[i];
				int count = group.GetDescendantCount();
 
				if (index + count >= globalIndex)
				{
					leftOver = globalIndex - index;
					return i;
				}
 
				index += count + 1;
			}
 
			return -1;
		}
 
		public event NotifyCollectionChangedEventHandler GroupedCollectionChanged;
		event NotifyCollectionChangedEventHandler ITemplatedItemsList<TItem>.GroupedCollectionChanged
		{
			add { GroupedCollectionChanged += value; }
			remove { GroupedCollectionChanged -= value; }
		}
 
		public int IndexOf(TItem item)
		{
			TemplatedItemsList<TView, TItem> group = GetGroup(item);
			if (group != null && group != this)
				return -1;
 
			return GetIndex(item);
		}
 
		[EditorBrowsable(EditorBrowsableState.Never)]
		public DataTemplate SelectDataTemplate(object item)
		{
			return ItemTemplate.SelectDataTemplate(item, _itemsView);
		}
 
		public TItem ActivateContent(int index, object item)
		{
			if (ItemTemplate?.CreateContent(item, _itemsView) is not TItem content)
			{
				content = _itemsView.CreateDefault(item);
			}
			content = UpdateContent(content, index, item);
 
			return content;
		}
 
		public TItem CreateContent(int index, object item, bool insert = false)
		{
			var content = ActivateContent(index, item);
 
			if ((CachingStrategy & ListViewCachingStrategy.RecycleElement) != 0)
				return content;
 
			for (int i = _templatedObjects.Count; i <= index; i++)
				_templatedObjects.Add(null);
 
			if (!insert)
				_templatedObjects[index] = content;
			else
				_templatedObjects.Insert(index, content);
 
			return content;
		}
 
		internal void ForceUpdate()
		{
			ListProxy.Clear();
		}
 
		internal TemplatedItemsList<TView, TItem> GetGroup(int index)
		{
			if (!IsGroupingEnabled)
				return this;
 
			return _groupedItems[index];
		}
 
		ITemplatedItemsList<TItem> ITemplatedItemsList<TItem>.GetGroup(int index)
		{
			return GetGroup(index);
		}
 
		internal static TemplatedItemsList<TView, TItem> GetGroup(TItem item)
		{
			if (item == null)
				throw new ArgumentNullException(nameof(item));
 
			return (TemplatedItemsList<TView, TItem>)item.GetValue(GroupProperty);
		}
 
		internal static int GetIndex(TItem item)
		{
			if (item == null)
				throw new ArgumentNullException(nameof(item));
 
			return (int)item.GetValue(IndexProperty);
		}
 
		internal static bool GetIsGroupHeader(BindableObject bindable)
		{
			return (bool)bindable.GetValue(IsGroupHeaderPropertyKey.BindableProperty);
		}
 
		internal TItem GetOrCreateContent(int index, object item)
		{
			TItem content;
			if (_templatedObjects.Count <= index || (content = _templatedObjects[index]) == null)
				content = CreateContent(index, item);
 
			return content;
		}
 
		internal static void SetIsGroupHeader(BindableObject bindable, bool value)
		{
			bindable.SetValue(IsGroupHeaderPropertyKey, value);
		}
 
		internal TItem UpdateContent(TItem content, int index, object item)
		{
			content.BindingContext = item;
 
			if (Parent != null)
				SetGroup(content, this);
 
			SetIndex(content, index);
 
			_itemsView.SetupContent(content, index);
 
			return content;
		}
 
		internal TItem UpdateContent(TItem content, int index)
		{
			object item = ListProxy[index];
			return UpdateContent(content, index, item);
		}
		TItem ITemplatedItemsList<TItem>.UpdateContent(TItem content, int index)
		{
			return UpdateContent(content, index);
		}
 
		internal TItem UpdateHeader(TItem content, int groupIndex)
		{
			if (Parent != null && Parent.GroupHeaderTemplate == null)
			{
				content.BindingContext = this;
			}
			else
			{
				content.BindingContext = ListProxy.ProxiedEnumerable;
			}
 
			SetIndex(content, groupIndex);
 
			_itemsView.SetupContent(content, groupIndex);
 
			return content;
		}
		TItem ITemplatedItemsList<TItem>.UpdateHeader(TItem content, int groupIndex)
		{
			return UpdateHeader(content, groupIndex);
		}
 
		void BindableOnPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (_itemSourceProperty != null && e.PropertyName == _itemSourceProperty.PropertyName)
				OnItemsSourceChanged();
			else if (e.PropertyName == _itemTemplateProperty.PropertyName)
				OnItemTemplateChanged();
			else if (ProgressiveLoadingProperty != null && e.PropertyName == ProgressiveLoadingProperty.PropertyName)
				OnInfiniteScrollingChanged();
			else if (GroupHeaderTemplateProperty != null && e.PropertyName == GroupHeaderTemplateProperty.PropertyName)
				OnHeaderTemplateChanged();
			else if (IsGroupingEnabledProperty != null && e.PropertyName == IsGroupingEnabledProperty.PropertyName)
				OnGroupingEnabledChanged();
		}
 
		IList ConvertContent(int startingIndex, IList items, bool forceCreate = false, bool setIndex = false)
		{
			var contentItems = new List<TItem>(items.Count);
			for (var i = 0; i < items.Count; i++)
			{
				int index = i + startingIndex;
				TItem content = !forceCreate ? GetOrCreateContent(index, items[i]) : CreateContent(index, items[i]);
				if (setIndex)
					SetIndex(content, index);
 
				contentItems.Add(content);
			}
 
			return contentItems;
		}
 
		IEnumerable GetItemsViewSource()
		{
			return (IEnumerable)_itemsView.GetValue(_itemSourceProperty);
		}
 
		object ITemplatedItemsList<TItem>.BindingContext
		{
			get
			{
				return BindingContext;
			}
		}
 
		void GroupedReset()
		{
			if (_groupedItems != null)
			{
				foreach (KeyValuePair<object, TemplatedItemsList<TView, TItem>> group in _groupedItems)
				{
					group.Value.CollectionChanged -= OnInnerCollectionChanged;
					group.Value.Dispose();
				}
				_groupedItems.Clear();
			}
 
			_templatedObjects.Clear();
 
			var i = 0;
			foreach (object item in ListProxy)
				InsertGrouped(item, i++);
 
			OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
		}
 
		TemplatedItemsList<TView, TItem> InsertGrouped(object item, int index)
		{
			var children = item as IEnumerable;
 
			var groupProxy = new TemplatedItemsList<TView, TItem>(this, children, _itemsView, _itemTemplateProperty);
			if (GroupDisplayBinding != null)
				groupProxy.SetBinding(NameProperty, GroupDisplayBinding.Clone());
			else if (GroupHeaderTemplate == null && item != null)
				groupProxy.Name = item.ToString();
 
			if (GroupShortNameBinding != null)
				groupProxy.SetBinding(ShortNameProperty, GroupShortNameBinding.Clone());
 
			groupProxy.BindingContext = item;
 
			if (GroupHeaderTemplate != null)
			{
				groupProxy.HeaderContent = (TItem)GroupHeaderTemplate.CreateContent(groupProxy.ItemsSource, _itemsView);
				groupProxy.HeaderContent.BindingContext = groupProxy.ItemsSource;
				//groupProxy.HeaderContent.BindingContext = groupProxy;
				//groupProxy.HeaderContent.SetBinding (BindingContextProperty, "ItemsSource");
			}
			else
			{
				// HACK: TemplatedItemsList shouldn't assume what the default is, but it needs
				// to be able to setup bindings. Needs some internal-API tweaking there isn't
				// time for right now.
				groupProxy.HeaderContent = _itemsView.CreateDefault(ListProxy.ProxiedEnumerable);
				groupProxy.HeaderContent.BindingContext = groupProxy;
				// TODO: the interceptor doesn't support generics at the moment
				// groupProxy.HeaderContent.SetBinding(TextCell.TextProperty, static (TemplatedItemsList<TView, TItem> list) => list.Name);
				groupProxy.HeaderContent.SetBinding(
					TextCell.TextProperty,
					TypedBinding.ForSingleNestingLevel(
						nameof(TemplatedItemsList<TView, TItem>.Name),
						getter: static (TemplatedItemsList<TView, TItem> list) => list.Name,
						setter: static (list, val) => list.Name = val));
			}
 
			SetIndex(groupProxy.HeaderContent, index);
			SetIsGroupHeader(groupProxy.HeaderContent, true);
 
			_itemsView.SetupContent(groupProxy.HeaderContent, index);
 
			_templatedObjects.Insert(index, groupProxy.HeaderContent);
			_groupedItems.Insert(index, item, groupProxy);
 
			groupProxy.CollectionChanged += OnInnerCollectionChanged;
 
			return groupProxy;
		}
 
		void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
		{
			NotifyCollectionChangedEventHandler changed = CollectionChanged;
			if (changed != null)
				changed(this, e);
		}
 
		void OnCollectionChangedGrouped(NotifyCollectionChangedEventArgs e)
		{
			if (_groupedItems == null)
				_groupedItems = new Cadenza.Collections.OrderedDictionary<object, TemplatedItemsList<TView, TItem>>();
 
			List<TemplatedItemsList<TView, TItem>> newItems = null, oldItems = null;
 
			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:
					if (e.NewStartingIndex == -1)
						goto case NotifyCollectionChangedAction.Reset;
 
					for (int i = e.NewStartingIndex; i < _templatedObjects.Count; i++)
						SetIndex(_templatedObjects[i], i + e.NewItems.Count);
 
					newItems = new List<TemplatedItemsList<TView, TItem>>(e.NewItems.Count);
 
					for (var i = 0; i < e.NewItems.Count; i++)
					{
						TemplatedItemsList<TView, TItem> converted = InsertGrouped(e.NewItems[i], e.NewStartingIndex + i);
						newItems.Add(converted);
					}
 
					OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, e.NewStartingIndex));
 
					break;
 
				case NotifyCollectionChangedAction.Remove:
					if (e.OldStartingIndex == -1)
						goto case NotifyCollectionChangedAction.Reset;
 
					int removeIndex = e.OldStartingIndex;
					for (int i = removeIndex + e.OldItems.Count; i < _templatedObjects.Count; i++)
						SetIndex(_templatedObjects[i], removeIndex++);
 
					oldItems = new List<TemplatedItemsList<TView, TItem>>(e.OldItems.Count);
					for (var i = 0; i < e.OldItems.Count; i++)
					{
						int index = e.OldStartingIndex;
						TemplatedItemsList<TView, TItem> til = _groupedItems[index];
						til.CollectionChanged -= OnInnerCollectionChanged;
						oldItems.Add(til);
						_groupedItems.RemoveAt(index);
						_templatedObjects.RemoveAt(index);
						til.Dispose();
					}
 
					OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems, e.OldStartingIndex));
 
					break;
 
				case NotifyCollectionChangedAction.Replace:
					if (e.OldStartingIndex == -1)
						goto case NotifyCollectionChangedAction.Reset;
 
					oldItems = new List<TemplatedItemsList<TView, TItem>>(e.OldItems.Count);
					newItems = new List<TemplatedItemsList<TView, TItem>>(e.NewItems.Count);
 
					for (var i = 0; i < e.OldItems.Count; i++)
					{
						int index = e.OldStartingIndex + i;
 
						TemplatedItemsList<TView, TItem> til = _groupedItems[index];
						til.CollectionChanged -= OnInnerCollectionChanged;
						oldItems.Add(til);
 
						_groupedItems.RemoveAt(index);
						_templatedObjects.RemoveAt(index);
 
						newItems.Add(InsertGrouped(e.NewItems[i], index));
						til.Dispose();
					}
 
					OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItems, oldItems, e.OldStartingIndex));
 
					break;
 
				case NotifyCollectionChangedAction.Move:
					if (e.OldStartingIndex == -1 || e.NewStartingIndex == -1)
						goto case NotifyCollectionChangedAction.Reset;
 
					bool movingForward = e.OldStartingIndex < e.NewStartingIndex;
 
					if (movingForward)
					{
						int moveIndex = e.OldStartingIndex;
						for (int i = moveIndex + e.OldItems.Count; i <= e.NewStartingIndex; i++)
							SetIndex(_templatedObjects[i], moveIndex++);
					}
					else
					{
						for (var i = 0; i < e.OldStartingIndex - e.NewStartingIndex; i++)
						{
							TItem item = _templatedObjects[i + e.NewStartingIndex];
							SetIndex(item, GetIndex(item) + e.OldItems.Count);
						}
					}
 
					oldItems = new List<TemplatedItemsList<TView, TItem>>(e.OldItems.Count);
 
					for (var i = 0; i < e.OldItems.Count; i++)
					{
						oldItems.Add(_groupedItems[e.OldStartingIndex]);
 
						_templatedObjects.RemoveAt(e.OldStartingIndex);
						_groupedItems.RemoveAt(e.OldStartingIndex);
					}
 
					int insertIndex = e.NewStartingIndex;
					if (e.OldStartingIndex < e.NewStartingIndex)
						insertIndex -= e.OldItems.Count - 1;
 
					for (var i = 0; i < oldItems.Count; i++)
					{
						TemplatedItemsList<TView, TItem> til = oldItems[i];
						_templatedObjects.Insert(insertIndex + i, til.HeaderContent);
						_groupedItems.Insert(insertIndex + i, til.BindingContext, til);
						SetIndex(til.HeaderContent, insertIndex + i);
					}
 
					OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, oldItems, e.OldStartingIndex, e.NewStartingIndex));
 
					break;
 
				case NotifyCollectionChangedAction.Reset:
					GroupedReset();
					break;
			}
		}
 
		void OnGroupingEnabledChanged()
		{
			if ((CachingStrategy & ListViewCachingStrategy.RecycleElement) != 0)
				_templatedObjects.Clear();
 
			OnItemsSourceChanged(true);
 
			if (!IsGroupingEnabled && _shortNames != null)
			{
				_shortNames.Dispose();
				_shortNames = null;
			}
			else
				OnShortNameBindingChanged();
		}
 
		void OnHeaderTemplateChanged()
		{
			OnItemTemplateChanged();
		}
 
		void OnInfiniteScrollingChanged()
		{
			OnItemsSourceChanged();
		}
 
		void OnInnerCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
			=> GroupedCollectionChanged?.Invoke(sender, e);
 
		void OnItemsSourceChanged(bool fromGrouping = false)
		{
			ListProxy.CollectionChanged -= OnProxyCollectionChanged;
 
			IEnumerable itemSource = GetItemsViewSource();
			if (itemSource == null)
				ListProxy = new ListProxy(Array.Empty<object>(), dispatcher: _itemsView.Dispatcher);
			else
				ListProxy = new ListProxy(itemSource, dispatcher: _itemsView.Dispatcher);
 
			ListProxy.CollectionChanged += OnProxyCollectionChanged;
			OnProxyCollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
		}
 
		void OnItemTemplateChanged()
		{
			if (ListProxy.Count == 0)
				return;
 
			OnProxyCollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
		}
 
		static void OnListProxyChanged(BindableObject bindable, object oldValue, object newValue)
		{
			var til = (TemplatedItemsList<TView, TItem>)bindable;
			til.OnPropertyChanged(nameof(ItemsSource));
		}
 
		void OnProxyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			if (IsGroupingEnabled)
			{
				OnCollectionChangedGrouped(e);
				return;
			}
 
			if ((CachingStrategy & ListViewCachingStrategy.RecycleElement) != 0)
			{
				OnCollectionChanged(e);
				return;
			}
 
			int count = Count;
			var ex = e as NotifyCollectionChangedEventArgsEx;
			if (ex != null)
				count = ex.Count;
 
			var maxindex = 0;
			if (e.NewStartingIndex >= 0 && e.NewItems != null)
				maxindex = Math.Max(maxindex, e.NewStartingIndex + e.NewItems.Count);
			if (e.OldStartingIndex >= 0 && e.OldItems != null)
				maxindex = Math.Max(maxindex, e.OldStartingIndex + e.OldItems.Count);
			if (maxindex > _templatedObjects.Count)
				_templatedObjects.InsertRange(_templatedObjects.Count, Enumerable.Repeat<TItem>(null, maxindex - _templatedObjects.Count));
 
			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:
					if (e.NewStartingIndex >= 0)
					{
						for (int i = e.NewStartingIndex; i < _templatedObjects.Count; i++)
							SetIndex(_templatedObjects[i], i + e.NewItems.Count);
 
						_templatedObjects.InsertRange(e.NewStartingIndex, Enumerable.Repeat<TItem>(null, e.NewItems.Count));
 
						IList items = ConvertContent(e.NewStartingIndex, e.NewItems, true, true);
						e = new NotifyCollectionChangedEventArgsEx(count, NotifyCollectionChangedAction.Add, items, e.NewStartingIndex);
					}
					else
					{
						goto case NotifyCollectionChangedAction.Reset;
					}
 
					break;
 
				case NotifyCollectionChangedAction.Move:
					if (e.NewStartingIndex < 0 || e.OldStartingIndex < 0)
						goto case NotifyCollectionChangedAction.Reset;
 
					bool movingForward = e.OldStartingIndex < e.NewStartingIndex;
 
					if (movingForward)
					{
						int moveIndex = e.OldStartingIndex;
						for (int i = moveIndex + e.OldItems.Count; i <= e.NewStartingIndex; i++)
							SetIndex(_templatedObjects[i], moveIndex++);
					}
					else
					{
						for (var i = 0; i < e.OldStartingIndex - e.NewStartingIndex; i++)
						{
							TItem item = _templatedObjects[i + e.NewStartingIndex];
							if (item != null)
								SetIndex(item, GetIndex(item) + e.OldItems.Count);
						}
					}
 
					TItem[] itemsToMove = _templatedObjects.Skip(e.OldStartingIndex).Take(e.OldItems.Count).ToArray();
 
					_templatedObjects.RemoveRange(e.OldStartingIndex, e.OldItems.Count);
					_templatedObjects.InsertRange(e.NewStartingIndex, itemsToMove);
					for (var i = 0; i < itemsToMove.Length; i++)
						SetIndex(itemsToMove[i], e.NewStartingIndex + i);
 
					e = new NotifyCollectionChangedEventArgsEx(count, NotifyCollectionChangedAction.Move, itemsToMove, e.NewStartingIndex, e.OldStartingIndex);
					break;
 
				case NotifyCollectionChangedAction.Remove:
					if (e.OldStartingIndex >= 0)
					{
						int removeIndex = e.OldStartingIndex;
						for (int i = removeIndex + e.OldItems.Count; i < _templatedObjects.Count; i++)
							SetIndex(_templatedObjects[i], removeIndex++);
 
						var items = new TItem[e.OldItems.Count];
						for (var i = 0; i < items.Length; i++)
						{
							TItem item = _templatedObjects[e.OldStartingIndex + i];
							if (item == null)
								continue;
 
							UnhookItem(item);
							items[i] = item;
						}
 
						_templatedObjects.RemoveRange(e.OldStartingIndex, e.OldItems.Count);
						e = new NotifyCollectionChangedEventArgsEx(count, NotifyCollectionChangedAction.Remove, items, e.OldStartingIndex);
					}
					else
					{
						goto case NotifyCollectionChangedAction.Reset;
					}
					break;
 
				case NotifyCollectionChangedAction.Replace:
					if (e.NewStartingIndex >= 0)
					{
						IList oldItems = ConvertContent(e.NewStartingIndex, e.OldItems);
						IList newItems = ConvertContent(e.NewStartingIndex, e.NewItems, true, true);
 
						for (var i = 0; i < oldItems.Count; i++)
						{
							UnhookItem((TItem)oldItems[i]);
						}
 
						e = new NotifyCollectionChangedEventArgsEx(count, NotifyCollectionChangedAction.Replace, newItems, oldItems, e.NewStartingIndex);
					}
					else
					{
						goto case NotifyCollectionChangedAction.Reset;
					}
 
					break;
 
				case NotifyCollectionChangedAction.Reset:
					e = new NotifyCollectionChangedEventArgsEx(count, NotifyCollectionChangedAction.Reset);
					UnhookAndClear();
					break;
 
				default:
					throw new ArgumentOutOfRangeException();
			}
 
			OnCollectionChanged(e);
		}
 
		void OnShortNameBindingChanged()
		{
			if (!IsGroupingEnabled)
				return;
 
			if (GroupShortNameBinding != null && _shortNames == null)
				_shortNames = new ShortNamesProxy(this);
			else if (GroupShortNameBinding == null && _shortNames != null)
			{
				_shortNames.Dispose();
				_shortNames = null;
			}
 
			if (_groupedItems != null)
			{
				if (GroupShortNameBinding == null)
				{
					foreach (TemplatedItemsList<TView, TItem> list in _groupedItems.Values)
						list.SetValue(ShortNameProperty, null);
 
					return;
				}
 
				foreach (TemplatedItemsList<TView, TItem> list in _groupedItems.Values)
					list.SetBinding(ShortNameProperty, GroupShortNameBinding.Clone());
			}
 
			_shortNames?.Reset();
		}
 
		static void SetGroup(TItem item, TemplatedItemsList<TView, TItem> group)
		{
			if (item == null)
				throw new ArgumentNullException(nameof(item));
 
			item.SetValue(GroupProperty, group);
		}
 
		static void SetIndex(TItem item, int index)
		{
			if (item == null)
				return;
 
			item.SetValue(IndexProperty, index);
		}
 
		void SplitCollectionChangedItems(NotifyCollectionChangedEventArgs e)
		{
			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:
					if (e.NewStartingIndex < 0)
						goto default;
 
					for (var i = 0; i < e.NewItems.Count; i++)
						OnProxyCollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, e.NewItems[i], e.NewStartingIndex + i));
 
					break;
 
				case NotifyCollectionChangedAction.Remove:
					if (e.OldStartingIndex < 0)
						goto default;
 
					for (var i = 0; i < e.OldItems.Count; i++)
						OnProxyCollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, e.OldItems[i], e.OldStartingIndex + i));
 
					break;
 
				case NotifyCollectionChangedAction.Replace:
					if (e.OldStartingIndex < 0)
						goto default;
 
					for (var i = 0; i < e.OldItems.Count; i++)
						OnProxyCollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, e.NewItems[i], e.OldItems[i], e.OldStartingIndex + i));
 
					break;
 
				default:
					OnProxyCollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
					break;
			}
		}
 
		void UnhookAndClear()
		{
			for (var i = 0; i < _templatedObjects.Count; i++)
			{
				TItem item = _templatedObjects[i];
				if (item == null)
					continue;
 
				UnhookItem(item);
			}
 
			_templatedObjects.Clear();
		}
 
		async void UnhookItem(TItem item)
		{
			SetIndex(item, -1);
			_itemsView.UnhookContent(item);
 
			//Hack: the cell could still be visible on iOS because the cells are reloaded after this unhook 
			//this causes some visual updates caused by a null datacontext and default values like IsVisible
			if (DeviceInfo.Platform == DevicePlatform.iOS && CachingStrategy == ListViewCachingStrategy.RetainElement)
				await Task.Delay(100);
			item.BindingContext = null;
		}
 
		class ShortNamesProxy : IReadOnlyList<string>, INotifyCollectionChanged, IDisposable
		{
			readonly HashSet<TemplatedItemsList<TView, TItem>> _attachedItems = new HashSet<TemplatedItemsList<TView, TItem>>();
			readonly TemplatedItemsList<TView, TItem> _itemsList;
 
			readonly Dictionary<TemplatedItemsList<TView, TItem>, string> _oldNames = new Dictionary<TemplatedItemsList<TView, TItem>, string>();
 
			bool _disposed;
 
			internal ShortNamesProxy(TemplatedItemsList<TView, TItem> itemsList)
			{
				_itemsList = itemsList;
				_itemsList.CollectionChanged += OnItemsListCollectionChanged;
			}
 
			public void Dispose()
			{
				if (_disposed)
					return;
				_disposed = true;
 
				_itemsList.CollectionChanged -= OnItemsListCollectionChanged;
 
				ResetCore(false);
			}
 
			IEnumerator IEnumerable.GetEnumerator()
			{
				return GetEnumerator();
			}
 
			public IEnumerator<string> GetEnumerator()
			{
				if (_itemsList._groupedItems == null)
					yield break;
 
				foreach (TemplatedItemsList<TView, TItem> item in _itemsList._groupedItems.Values)
				{
					AttachList(item);
					yield return item.ShortName;
				}
			}
 
			public event NotifyCollectionChangedEventHandler CollectionChanged;
 
			public int Count
			{
				get { return _itemsList._groupedItems.Count; }
			}
 
			public string this[int index]
			{
				get
				{
					TemplatedItemsList<TView, TItem> list = _itemsList._groupedItems[index];
					AttachList(list);
 
					return list.ShortName;
				}
			}
 
			public void Reset()
			{
				ResetCore(true);
			}
 
			void AttachList(TemplatedItemsList<TView, TItem> list)
			{
				if (_attachedItems.Contains(list))
					return;
 
				list.PropertyChanging += OnChildListPropertyChanging;
				list.PropertyChanged += OnChildListPropertyChanged;
				_attachedItems.Add(list);
			}
 
			List<string> ConvertItems(IList list)
			{
				var newList = new List<string>(list.Count);
				newList.AddRange(list.Cast<TemplatedItemsList<TView, TItem>>().Select(tl => tl.ShortName));
				return newList;
			}
 
			void OnChildListPropertyChanged(object sender, PropertyChangedEventArgs e)
			{
				if (e.PropertyName != ShortNameProperty.PropertyName)
					return;
 
				var list = (TemplatedItemsList<TView, TItem>)sender;
				string old = _oldNames[list];
				_oldNames.Remove(list);
 
				int index = _itemsList._groupedItems.Values.IndexOf(list);
 
				OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, list.ShortName, old, index));
			}
 
			void OnChildListPropertyChanging(object sender, PropertyChangingEventArgs e)
			{
				if (e.PropertyName != ShortNameProperty.PropertyName)
					return;
 
				var list = (TemplatedItemsList<TView, TItem>)sender;
				_oldNames[list] = list.ShortName;
			}
 
			void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
			{
				NotifyCollectionChangedEventHandler changed = CollectionChanged;
				if (changed != null)
					changed(this, e);
			}
 
			void OnItemsListCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
			{
				switch (e.Action)
				{
					case NotifyCollectionChangedAction.Add:
						e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, ConvertItems(e.NewItems), e.NewStartingIndex);
						break;
 
					case NotifyCollectionChangedAction.Move:
						e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, ConvertItems(e.OldItems), e.NewStartingIndex, e.OldStartingIndex);
						break;
 
					case NotifyCollectionChangedAction.Remove:
						e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, ConvertItems(e.OldItems), e.OldStartingIndex);
						break;
 
					case NotifyCollectionChangedAction.Replace:
						e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, ConvertItems(e.NewItems), ConvertItems(e.OldItems), e.OldStartingIndex);
						break;
				}
 
				OnCollectionChanged(e);
			}
 
			void ResetCore(bool raiseReset)
			{
				foreach (TemplatedItemsList<TView, TItem> list in _attachedItems)
				{
					list.PropertyChanged -= OnChildListPropertyChanged;
					list.PropertyChanging -= OnChildListPropertyChanging;
				}
 
				_attachedItems.Clear();
 
				if (raiseReset)
					OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
			}
		}
	}
}