| File: Shell\ShellElementCollection.cs | Web Access |
| Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls) |
#nullable disable using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; namespace Microsoft.Maui.Controls { internal abstract class ShellElementCollection : IList<BaseShellItem>, INotifyCollectionChanged { public event NotifyCollectionChangedEventHandler VisibleItemsChangedInternal; readonly List<NotifyCollectionChangedEventArgs> _notifyCollectionChangedEventArgs; bool _pauseCollectionChanged; public event NotifyCollectionChangedEventHandler CollectionChanged; public event NotifyCollectionChangedEventHandler VisibleItemsChanged; public int Count => Inner.Count; public bool IsReadOnly => Inner.IsReadOnly; IList _inner; IList _visibleItems; protected ShellElementCollection() { _notifyCollectionChangedEventArgs = new List<NotifyCollectionChangedEventArgs>(); } internal IList Inner { get => _inner; private protected set { if (_inner != null) throw new ArgumentException("Inner can only be set once"); _inner = value; ((INotifyCollectionChanged)_inner).CollectionChanged += InnerCollectionChanged; } } protected void OnVisibleItemsChanged(object sender, NotifyCollectionChangedEventArgs args) { if (args?.NewItems?.Count > 0 && _pauseCollectionChanged) { _notifyCollectionChangedEventArgs.Add(args); ResumeCollectionChanged(); return; } if (_pauseCollectionChanged) { _notifyCollectionChangedEventArgs.Add(args); return; } VisibleItemsChangedInternal?.Invoke(VisibleItemsReadOnly, args); VisibleItemsChanged?.Invoke(VisibleItemsReadOnly, args); } protected IList VisibleItems { get => _visibleItems; private protected set { _visibleItems = value; ((INotifyCollectionChanged)_visibleItems).CollectionChanged += OnVisibleItemsChanged; } } public IReadOnlyCollection<BaseShellItem> VisibleItemsReadOnly { get; private protected set; } // Pause Collection Changed events when the list has zero items // we don't want to propagate out a visible collection changed event until the next visible item // is realized void PauseCollectionChanged() => _pauseCollectionChanged = true; void ResumeCollectionChanged() { _pauseCollectionChanged = false; // process the added items first and then remove var pendingEvents = _notifyCollectionChangedEventArgs.OrderBy(x => x.NewItems != null ? 0 : 1).ToList(); _notifyCollectionChangedEventArgs.Clear(); foreach (var args in pendingEvents) VisibleItemsChangedInternal?.Invoke(VisibleItemsReadOnly, args); foreach (var args in pendingEvents) VisibleItemsChanged?.Invoke(VisibleItemsReadOnly, args); } #region IList public BaseShellItem this[int index] { get => (BaseShellItem)Inner[index]; set => Inner[index] = value; } public void Clear() { var list = Inner.Cast<BaseShellItem>().ToList(); try { PauseCollectionChanged(); Removing(Inner); } finally { ResumeCollectionChanged(); } Inner.Clear(); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, list)); } public virtual void Add(BaseShellItem item) => Inner.Add(item); public virtual bool Contains(BaseShellItem item) => Inner.Contains(item); public virtual void CopyTo(BaseShellItem[] array, int arrayIndex) => Inner.CopyTo(array, arrayIndex); public abstract IEnumerator<BaseShellItem> GetEnumerator(); public virtual int IndexOf(BaseShellItem item) => Inner.IndexOf(item); public virtual void Insert(int index, BaseShellItem item) => Inner.Insert(index, item); public abstract bool Remove(BaseShellItem item); public virtual void RemoveAt(int index) => Inner.RemoveAt(index); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Inner).GetEnumerator(); #endregion void InnerCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) { foreach (BaseShellItem element in e.NewItems) { if (element is IElementController controller) OnElementControllerInserting(controller); CheckVisibility(element); } } if (e.OldItems != null) { Removing(e.OldItems); } CollectionChanged?.Invoke(this, e); } void Removing(IEnumerable items) { foreach (BaseShellItem element in items) { if (VisibleItems.Contains(element)) VisibleItems.Remove(element); if (element is IElementController controller) OnElementControllerRemoving(controller); } } protected void CheckVisibility(BaseShellItem element) { if (IsBaseShellItemVisible(element)) { if (VisibleItems.Contains(element)) return; int visibleIndex = 0; for (var i = 0; i < Inner.Count; i++) { var item = Inner[i]; if (!IsBaseShellItemVisible(element)) continue; if (item == element) { VisibleItems.Insert(visibleIndex, element); break; } if (VisibleItems.Contains(item)) visibleIndex++; } } else if (VisibleItems.Contains(element)) { VisibleItems.Remove(element); } bool IsBaseShellItemVisible(BaseShellItem item) { if (!item.IsVisible) return false; if (item is ShellGroupItem sgi) { return (sgi.ShellElementCollection.VisibleItemsReadOnly.Count > 0) || item is IMenuItemController; } return IsShellElementVisible(item); } } protected virtual bool IsShellElementVisible(BaseShellItem item) { return false; } protected virtual void OnElementControllerInserting(IElementController controller) { if (controller is ShellGroupItem sgi) { sgi.ShellElementCollection.VisibleItemsChangedInternal += OnShellElementControllerItemsCollectionChanged; } if (controller is BaseShellItem bsi) bsi.PropertyChanged += BaseShellItemPropertyChanged; } protected virtual void OnElementControllerRemoving(IElementController controller) { if (controller is ShellGroupItem sgi) { sgi.ShellElementCollection.VisibleItemsChangedInternal -= OnShellElementControllerItemsCollectionChanged; } if (controller is BaseShellItem bsi) bsi.PropertyChanged -= BaseShellItemPropertyChanged; } void BaseShellItemPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(BaseShellItem.IsVisible)) CheckVisibility((BaseShellItem)sender); } void OnShellElementControllerItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { foreach (BaseShellItem bsi in (e.NewItems ?? e.OldItems ?? Inner)) { if (bsi.Parent == null) bsi.ParentSet += OnParentSet; else CheckVisibility(bsi.Parent as BaseShellItem); } void OnParentSet(object s, EventArgs __) { var baseShellItem = (BaseShellItem)s; baseShellItem.ParentSet -= OnParentSet; CheckVisibility(baseShellItem.Parent as BaseShellItem); } } } internal abstract class ShellElementCollection<TBaseShellItem> : ShellElementCollection, IList<TBaseShellItem> where TBaseShellItem : BaseShellItem { public ShellElementCollection() { var items = new ObservableCollection<TBaseShellItem>(); VisibleItems = items; VisibleItemsReadOnly = new ReadOnlyCollection<TBaseShellItem>(items); } public new ReadOnlyCollection<TBaseShellItem> VisibleItemsReadOnly { get => (ReadOnlyCollection<TBaseShellItem>)base.VisibleItemsReadOnly; private protected set => base.VisibleItemsReadOnly = value; } internal new IList<TBaseShellItem> Inner { get => (IList<TBaseShellItem>)base.Inner; set => base.Inner = (IList)value; } TBaseShellItem IList<TBaseShellItem>.this[int index] { get => (TBaseShellItem)Inner[index]; set => Inner[index] = value; } public virtual void Add(TBaseShellItem item) => Inner.Add(item); public virtual bool Contains(TBaseShellItem item) => Inner.Contains(item); public virtual void CopyTo(TBaseShellItem[] array, int arrayIndex) => Inner.CopyTo(array, arrayIndex); public virtual int IndexOf(TBaseShellItem item) => Inner.IndexOf(item); public virtual void Insert(int index, TBaseShellItem item) => Inner.Insert(index, item); public virtual bool Remove(TBaseShellItem item) => Inner.Remove(item); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Inner).GetEnumerator(); IEnumerator<TBaseShellItem> IEnumerable<TBaseShellItem>.GetEnumerator() { return Inner.GetEnumerator(); } public override IEnumerator<BaseShellItem> GetEnumerator() { return Inner.Cast<BaseShellItem>().GetEnumerator(); } public override bool Remove(BaseShellItem item) { return Remove((TBaseShellItem)item); } } }