using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; namespace Xamarin.Forms.Pages { public class CompoundCollection : Element, IList, INotifyCollectionChanged { public static readonly BindableProperty MainListProperty = BindableProperty.Create(nameof(MainList), typeof(IReadOnlyList), typeof(CompoundCollection), default(IReadOnlyList), propertyChanged: OnMainListPropertyChanged); readonly ObservableCollection _appendList = new ObservableCollection(); readonly ObservableCollection _prependList = new ObservableCollection(); public CompoundCollection() { _prependList.CollectionChanged += OnPrependCollectionChanged; _appendList.CollectionChanged += OnAppendCollectionChanged; } public IList AppendList => _appendList; public IReadOnlyList MainList { get { return (IReadOnlyList)GetValue(MainListProperty); } set { SetValue(MainListProperty, value); } } public IList PrependList => _prependList; public void CopyTo(Array array, int index) { throw new NotSupportedException(); } public int Count => AppendList.Count + PrependList.Count + (MainList?.Count ?? 0); public bool IsSynchronized => false; public object SyncRoot => null; public IEnumerator GetEnumerator() { foreach (object item in PrependList) yield return item; foreach (object item in MainList) yield return item; foreach (object item in AppendList) yield return item; } public int Add(object value) { throw new NotSupportedException(); } public void Clear() { throw new NotSupportedException(); } public bool Contains(object value) { IReadOnlyList mainList = MainList; bool masterContains; var masterList = mainList as IList; if (masterList != null) { masterContains = masterList.Contains(value); } else { masterContains = mainList.Contains(value); } return masterContains || PrependList.Contains(value) || AppendList.Contains(value); } public int IndexOf(object value) { int result; result = PrependList.IndexOf(value); if (result >= 0) return result; result = MainList.IndexOf(value); if (result >= 0) return result + PrependList.Count; result = AppendList.IndexOf(value); if (result >= 0) return result + PrependList.Count + MainList.Count; return -1; } public void Insert(int index, object value) { throw new NotSupportedException(); } public bool IsFixedSize => false; public bool IsReadOnly => true; public object this[int index] { get { IReadOnlyList mainList = MainList; int prependSize = PrependList.Count; if (index < prependSize) return PrependList[index]; index -= prependSize; if (mainList != null) { if (index < mainList.Count) return mainList[index]; index -= mainList.Count; } if (index >= AppendList.Count) throw new IndexOutOfRangeException(); return AppendList[index]; } set { throw new NotSupportedException(); } } public void Remove(object value) { throw new NotSupportedException(); } public void RemoveAt(int index) { throw new NotSupportedException(); } public event NotifyCollectionChangedEventHandler CollectionChanged; void OnAppendCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) { int offset = _prependList.Count + (MainList?.Count ?? 0); // here we just need to calculate the offset for the index, everything else is the same switch (args.Action) { case NotifyCollectionChangedAction.Add: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, args.NewItems, offset + args.NewStartingIndex)); break; case NotifyCollectionChangedAction.Move: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, args.OldItems, offset + args.NewStartingIndex, offset + args.OldStartingIndex)); break; case NotifyCollectionChangedAction.Remove: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, args.OldItems, offset + args.OldStartingIndex)); break; case NotifyCollectionChangedAction.Replace: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, args.NewItems, args.OldItems, offset + args.OldStartingIndex)); break; case NotifyCollectionChangedAction.Reset: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); break; default: throw new ArgumentOutOfRangeException(); } } void OnCollectionChanged(NotifyCollectionChangedEventArgs args) { CollectionChanged?.Invoke(this, args); } void OnMainCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) { // much complexity to be had here switch (args.Action) { case NotifyCollectionChangedAction.Add: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, args.NewItems, PublicIndexFromMainIndex(args.NewStartingIndex))); break; case NotifyCollectionChangedAction.Move: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, args.OldItems, PublicIndexFromMainIndex(args.NewStartingIndex), PublicIndexFromMainIndex(args.OldStartingIndex))); break; case NotifyCollectionChangedAction.Remove: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, args.OldItems, PublicIndexFromMainIndex(args.OldStartingIndex))); break; case NotifyCollectionChangedAction.Replace: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, args.NewItems, args.OldItems, PublicIndexFromMainIndex(args.OldStartingIndex))); break; case NotifyCollectionChangedAction.Reset: OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); break; default: throw new ArgumentOutOfRangeException(); } } static void OnMainListPropertyChanged(BindableObject bindable, object oldValue, object newValue) { var self = (CompoundCollection)bindable; var observable = oldValue as INotifyCollectionChanged; if (observable != null) observable.CollectionChanged -= self.OnMainCollectionChanged; observable = newValue as INotifyCollectionChanged; if (observable != null) observable.CollectionChanged += self.OnMainCollectionChanged; self.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } void OnPrependCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) { // this can basically be a passthrough as prepend has no masking and identical indexing OnCollectionChanged(args); } int PublicIndexFromMainIndex(int index) { return PrependList.Count + index; } } }