using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Cadenza.Collections; namespace Xamarin.Forms { internal sealed class TemplatedItemsList : BindableObject, IReadOnlyList, IList, INotifyCollectionChanged, IDisposable where TView : BindableObject, IItemsView where TItem : BindableObject { public static readonly BindableProperty NameProperty = BindableProperty.Create("Name", typeof(string), typeof(TemplatedItemsList), null); public static readonly BindableProperty ShortNameProperty = BindableProperty.Create("ShortName", typeof(string), typeof(TemplatedItemsList), null); static readonly BindablePropertyKey HeaderContentPropertyKey = BindableProperty.CreateReadOnly("HeaderContent", typeof(TItem), typeof(TemplatedItemsList), null); internal static readonly BindablePropertyKey ListProxyPropertyKey = BindableProperty.CreateReadOnly("ListProxy", typeof(ListProxy), typeof(TemplatedItemsList), null, propertyChanged: OnListProxyChanged); static readonly BindableProperty GroupProperty = BindableProperty.Create("Group", typeof(TemplatedItemsList), 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 _templatedObjects = new List(); bool _disposed; BindingBase _groupDisplayBinding; OrderedDictionary> _groupedItems; DataTemplate _groupHeaderTemplate; BindingBase _groupShortNameBinding; ShortNamesProxy _shortNames; internal TemplatedItemsList(TView itemsView, BindableProperty itemSourceProperty, BindableProperty itemTemplateProperty) { if (itemsView == null) throw new ArgumentNullException("itemsView"); if (itemSourceProperty == null) throw new ArgumentNullException("itemSourceProperty"); if (itemTemplateProperty == null) throw new ArgumentNullException("itemTemplateProperty"); _itemsView = itemsView; _itemsView.PropertyChanged += BindableOnPropertyChanged; _itemSourceProperty = itemSourceProperty; _itemTemplateProperty = itemTemplateProperty; IEnumerable source = GetItemsViewSource(); if (source != null) ListProxy = new ListProxy(source); else ListProxy = new ListProxy(new object[0]); } internal TemplatedItemsList(TemplatedItemsList parent, IEnumerable itemSource, TView itemsView, BindableProperty itemTemplateProperty, int windowSize = int.MaxValue) { if (itemsView == null) throw new ArgumentNullException("itemsView"); if (itemTemplateProperty == null) throw new ArgumentNullException("itemTemplateProperty"); Parent = parent; _itemsView = itemsView; _itemsView.PropertyChanged += BindableOnPropertyChanged; _itemTemplateProperty = itemTemplateProperty; if (itemSource != null) { ListProxy = new ListProxy(itemSource, windowSize); ListProxy.CollectionChanged += OnProxyCollectionChanged; } else ListProxy = new ListProxy(new object[0]); } 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; } 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 Parent { get; } public BindableProperty ProgressiveLoadingProperty { get; set; } public string ShortName { get { return (string)GetValue(ShortNameProperty); } set { SetValue(ShortNameProperty, value); } } public IReadOnlyList ShortNames { get { return _shortNames; } } internal ListViewCachingStrategy CachingStrategy { get { var listView = _itemsView as ListView; if (listView == null) return ListViewCachingStrategy.RetainElement; return listView.CachingStrategy; } } internal ListProxy ListProxy { get { return (ListProxy)GetValue(ListProxyPropertyKey.BindableProperty); } private set { SetValue(ListProxyPropertyKey, value); } } 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 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; 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 group in _groupedItems.Values) count += group.GetDescendantCount(); return count; } public int GetGlobalIndexForGroup(TemplatedItemsList group) { if (group == null) throw new ArgumentNullException("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 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> 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 GetGroupAndIndexOfItem(object item) { if (item == null) return new Tuple(-1, -1); if (!IsGroupingEnabled) return new Tuple(0, GetGlobalIndexOfItem(item)); var group = 0; if (_groupedItems != null) { foreach (TemplatedItemsList children in _groupedItems.Values) { int index = children.GetGlobalIndexOfItem(item); if (index != -1) return new Tuple(group, index); group++; } } return new Tuple(-1, -1); } public Tuple GetGroupAndIndexOfItem(object group, object item) { if (!IsGroupingEnabled) return new Tuple(0, GetGlobalIndexOfItem(item)); if (_groupedItems == null) return new Tuple(-1, -1); var groupIndex = 0; foreach (TemplatedItemsList 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(groupIndex, i); } if (group != null) return new Tuple(groupIndex, -1); } groupIndex++; } return new Tuple(-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 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; public int IndexOf(TItem item) { TemplatedItemsList group = GetGroup(item); if (group != null && group != this) return -1; return GetIndex(item); } internal TItem CreateContent(int index, object item, bool insert = false) { TItem content = ItemTemplate != null ? (TItem)ItemTemplate.CreateContent(item, _itemsView) : _itemsView.CreateDefault(item); content = UpdateContent(content, index, item); if (CachingStrategy == ListViewCachingStrategy.RecycleElement) 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 GetGroup(int index) { if (!IsGroupingEnabled) return this; return _groupedItems[index]; } internal static TemplatedItemsList GetGroup(TItem item) { if (item == null) throw new ArgumentNullException("item"); return (TemplatedItemsList)item.GetValue(GroupProperty); } internal static int GetIndex(TItem item) { if (item == null) throw new ArgumentNullException("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); } 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; } 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(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); } void GroupedReset() { if (_groupedItems != null) { foreach (KeyValuePair> 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 InsertGrouped(object item, int index) { var children = item as IEnumerable; var groupProxy = new TemplatedItemsList(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; groupProxy.HeaderContent.SetBinding(TextCell.TextProperty, "Name"); } 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 OrderedDictionary>(); List> 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>(e.NewItems.Count); for (var i = 0; i < e.NewItems.Count; i++) { TemplatedItemsList 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>(e.OldItems.Count); for (var i = 0; i < e.OldItems.Count; i++) { int index = e.OldStartingIndex + i; TemplatedItemsList 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>(e.OldItems.Count); newItems = new List>(e.NewItems.Count); for (var i = 0; i < e.OldItems.Count; i++) { int index = e.OldStartingIndex + i; TemplatedItemsList 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>(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 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) _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) { NotifyCollectionChangedEventHandler handler = GroupedCollectionChanged; if (handler != null) handler(sender, e); } void OnItemsSourceChanged(bool fromGrouping = false) { ListProxy.CollectionChanged -= OnProxyCollectionChanged; IEnumerable itemSource = GetItemsViewSource(); if (itemSource == null) ListProxy = new ListProxy(new object[0]); else ListProxy = new ListProxy(itemSource); 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)bindable; til.OnPropertyChanged("ItemsSource"); } void OnProxyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnProxyCollectionChanged(sender, e, true); } void OnProxyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e, bool fixWindows = true) { if (IsGroupingEnabled) { OnCollectionChangedGrouped(e); return; } if (CachingStrategy == ListViewCachingStrategy.RecycleElement) { OnCollectionChanged(e); return; } /* HACKAHACKHACK: LongListSelector on WP SL has a bug in that it completely fails to deal with * INCC notifications that include more than 1 item. */ if (fixWindows && Device.OS == TargetPlatform.WinPhone) { SplitCollectionChangedItems(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(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(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 list in _groupedItems.Values) list.SetValue(ShortNameProperty, null); return; } foreach (TemplatedItemsList list in _groupedItems.Values) list.SetBinding(ShortNameProperty, GroupShortNameBinding.Clone()); } if (_shortNames != null) _shortNames.Reset(); } static void SetGroup(TItem item, TemplatedItemsList group) { if (item == null) throw new ArgumentNullException("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), false); 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), false); 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), false); break; default: OnProxyCollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset), false); 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 (Device.OS == TargetPlatform.iOS && CachingStrategy == ListViewCachingStrategy.RetainElement) await Task.Delay(100); item.BindingContext = null; } class ShortNamesProxy : IReadOnlyList, INotifyCollectionChanged, IDisposable { readonly HashSet> _attachedItems = new HashSet>(); readonly TemplatedItemsList _itemsList; readonly Dictionary, string> _oldNames = new Dictionary, string>(); bool _disposed; internal ShortNamesProxy(TemplatedItemsList 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 GetEnumerator() { if (_itemsList._groupedItems == null) yield break; foreach (TemplatedItemsList 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 list = _itemsList._groupedItems[index]; AttachList(list); return list.ShortName; } } public void Reset() { ResetCore(true); } void AttachList(TemplatedItemsList list) { if (_attachedItems.Contains(list)) return; list.PropertyChanging += OnChildListPropertyChanging; list.PropertyChanged += OnChildListPropertyChanged; _attachedItems.Add(list); } List ConvertItems(IList list) { var newList = new List(list.Count); newList.AddRange(list.Cast>().Select(tl => tl.ShortName)); return newList; } void OnChildListPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName != ShortNameProperty.PropertyName) return; var list = (TemplatedItemsList)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)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 list in _attachedItems) { list.PropertyChanged -= OnChildListPropertyChanged; list.PropertyChanging -= OnChildListPropertyChanging; } _attachedItems.Clear(); if (raiseReset) OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } } }