diff options
author | Jason Smith <jason.smith@xamarin.com> | 2016-03-22 13:02:25 -0700 |
---|---|---|
committer | Jason Smith <jason.smith@xamarin.com> | 2016-03-22 16:13:41 -0700 |
commit | 17fdde66d94155fc62a034fa6658995bef6fd6e5 (patch) | |
tree | b5e5073a2a7b15cdbe826faa5c763e270a505729 /Xamarin.Forms.Core/TemplatedItemsList.cs | |
download | xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.tar.gz xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.tar.bz2 xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.zip |
Initial import
Diffstat (limited to 'Xamarin.Forms.Core/TemplatedItemsList.cs')
-rw-r--r-- | Xamarin.Forms.Core/TemplatedItemsList.cs | 1325 |
1 files changed, 1325 insertions, 0 deletions
diff --git a/Xamarin.Forms.Core/TemplatedItemsList.cs b/Xamarin.Forms.Core/TemplatedItemsList.cs new file mode 100644 index 00000000..814f5835 --- /dev/null +++ b/Xamarin.Forms.Core/TemplatedItemsList.cs @@ -0,0 +1,1325 @@ +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<TView, TItem> : BindableObject, IReadOnlyList<TItem>, IList, INotifyCollectionChanged, IDisposable where TView : BindableObject, IItemsView<TItem> + where TItem : BindableObject + { + public static readonly BindableProperty NameProperty = BindableProperty.Create("Name", typeof(string), typeof(TemplatedItemsList<TView, TItem>), null); + + public static readonly BindableProperty ShortNameProperty = BindableProperty.Create("ShortName", typeof(string), typeof(TemplatedItemsList<TView, TItem>), null); + + static readonly BindablePropertyKey HeaderContentPropertyKey = BindableProperty.CreateReadOnly("HeaderContent", typeof(TItem), typeof(TemplatedItemsList<TView, TItem>), null); + + internal static readonly BindablePropertyKey ListProxyPropertyKey = BindableProperty.CreateReadOnly("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; + 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("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<TView, TItem> 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<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 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<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(TemplatedItemsList<TView, TItem> 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<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; + + public int IndexOf(TItem item) + { + TemplatedItemsList<TView, TItem> 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<TView, TItem> GetGroup(int index) + { + if (!IsGroupingEnabled) + return this; + + return _groupedItems[index]; + } + + internal static TemplatedItemsList<TView, TItem> GetGroup(TItem item) + { + if (item == null) + throw new ArgumentNullException("item"); + + return (TemplatedItemsList<TView, TItem>)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<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); + } + + 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; + 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<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 + i; + 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) + _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<TView, TItem>)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<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()); + } + + if (_shortNames != null) + _shortNames.Reset(); + } + + static void SetGroup(TItem item, TemplatedItemsList<TView, TItem> 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<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)); + } + } + } +}
\ No newline at end of file |