File: MultiPage.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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls.Internals;
 
namespace Microsoft.Maui.Controls
{
	[ContentProperty("Children")]
	public abstract class MultiPage<[DynamicallyAccessedMembers(BindableProperty.DeclaringTypeMembers | BindableProperty.ReturnTypeMembers)] T> : Page, IViewContainer<T>, IPageContainer<T>, IItemsView<T>, IMultiPageController<T> where T : Page
	{
		/// <summary>Bindable property for <see cref="ItemsSource"/>.</summary>
		public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(MultiPage<>), null);
 
		/// <summary>Bindable property for <see cref="ItemTemplate"/>.</summary>
		public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(MultiPage<>), null);
 
		/// <summary>Bindable property for <see cref="SelectedItem"/>.</summary>
		public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create(nameof(SelectedItem), typeof(object), typeof(MultiPage<>), null, BindingMode.TwoWay);
 
		internal static readonly BindableProperty IndexProperty = BindableProperty.Create("Index", typeof(int), typeof(Page), -1);
 
		readonly ElementCollection<T> _children;
		readonly TemplatedItemsList<MultiPage<T>, T> _templatedItems;
 
		T _current;
 
		protected MultiPage()
		{
			_templatedItems = new TemplatedItemsList<MultiPage<T>, T>(this, ItemsSourceProperty, ItemTemplateProperty);
			_templatedItems.CollectionChanged += OnTemplatedItemsChanged;
 
			_children = new ElementCollection<T>(InternalChildren);
			InternalChildren.CollectionChanged += OnChildrenChanged;
		}
 
		public IEnumerable ItemsSource
		{
			get { return (IEnumerable)GetValue(ItemsSourceProperty); }
			set { SetValue(ItemsSourceProperty, value); }
		}
 
		public DataTemplate ItemTemplate
		{
			get { return (DataTemplate)GetValue(ItemTemplateProperty); }
			set { SetValue(ItemTemplateProperty, value); }
		}
 
		public object SelectedItem
		{
			get { return GetValue(SelectedItemProperty); }
			set { SetValue(SelectedItemProperty, value); }
		}
 
		T IItemsView<T>.CreateDefault(object item)
		{
			return CreateDefault(item);
		}
 
		void IItemsView<T>.SetupContent(T content, int index)
		{
			SetupContent(content, index);
		}
 
		void IItemsView<T>.UnhookContent(T content)
		{
			UnhookContent(content);
		}
 
		public T CurrentPage
		{
			get { return _current; }
			set
			{
				if (_current == value)
					return;
 
				var previousPage = _current;
				OnPropertyChanging();
 
				// TODO: MAUI refine this to fire earlier
				_current?.SendNavigatingFrom(new NavigatingFromEventArgs());
 
				_current = value;
 
				previousPage?.SendDisappearing();
 
				OnPropertyChanged();
				OnCurrentPageChanged();
 
				if (HasAppeared)
					_current?.SendAppearing();
 
				previousPage?.SendNavigatedFrom(new NavigatedFromEventArgs(_current, NavigationType.PageSwap));
				_current?.SendNavigatedTo(new NavigatedToEventArgs(previousPage));
			}
		}
 
		public IList<T> Children
		{
			get { return _children; }
		}
 
		public event EventHandler CurrentPageChanged;
 
		public event NotifyCollectionChangedEventHandler PagesChanged;
 
		protected abstract T CreateDefault(object item);
 
		protected override bool OnBackButtonPressed()
		{
			if (CurrentPage != null)
			{
				bool handled = CurrentPage.SendBackButtonPressed();
				if (handled)
					return true;
			}
 
			return base.OnBackButtonPressed();
		}
 
		protected override void OnChildAdded(Element child)
		{
			base.OnChildAdded(child);
 
			ForceLayout();
		}
 
		protected virtual void OnCurrentPageChanged()
		{
			EventHandler changed = CurrentPageChanged;
			if (changed != null)
				changed(this, EventArgs.Empty);
		}
 
		protected virtual void OnPagesChanged(NotifyCollectionChangedEventArgs e)
			=> PagesChanged?.Invoke(this, e);
 
		protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
		{
			if (propertyName == ItemsSourceProperty.PropertyName)
				_children.IsReadOnly = ItemsSource != null;
			else if (propertyName == SelectedItemProperty.PropertyName)
			{
				UpdateCurrentPage();
			}
			else if (propertyName == "CurrentPage" && ItemsSource != null)
			{
				if (CurrentPage == null)
				{
					SelectedItem = null;
				}
				else
				{
					int index = _templatedItems.IndexOf(CurrentPage);
					SelectedItem = index != -1 ? _templatedItems.ListProxy[index] : null;
				}
			}
 
			base.OnPropertyChanged(propertyName);
		}
 
		protected virtual void SetupContent(T content, int index)
		{
		}
 
		protected virtual void UnhookContent(T content)
		{
		}
 
		[EditorBrowsable(EditorBrowsableState.Never)]
		public static int GetIndex(T page)
		{
			if (page == null)
				throw new ArgumentNullException(nameof(page));
 
			return (int)page.GetValue(IndexProperty);
		}
 
		[EditorBrowsable(EditorBrowsableState.Never)]
		public T GetPageByIndex(int index)
		{
			foreach (T page in InternalChildren)
			{
				if (index == GetIndex(page))
					return page;
			}
			return null;
		}
 
		[EditorBrowsable(EditorBrowsableState.Never)]
		public static void SetIndex(Page page, int index)
		{
			if (page == null)
				throw new ArgumentNullException(nameof(page));
 
			page.SetValue(IndexProperty, index);
		}
 
		void OnChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			if (Children.IsReadOnly)
				return;
 
			var i = 0;
			foreach (T page in Children)
				SetIndex(page, i++);
 
			OnPagesChanged(e);
 
			if (CurrentPage == null || Children.IndexOf(CurrentPage) == -1)
				CurrentPage = Children.FirstOrDefault();
		}
 
		void OnTemplatedItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:
					if (e.NewStartingIndex < 0)
						goto case NotifyCollectionChangedAction.Reset;
 
					for (int i = e.NewStartingIndex; i < Children.Count; i++)
						SetIndex((T)InternalChildren[i], i + e.NewItems.Count);
 
					for (var i = 0; i < e.NewItems.Count; i++)
					{
						var page = (T)e.NewItems[i];
						page.Owned = true;
						int index = i + e.NewStartingIndex;
						SetIndex(page, index);
						InternalChildren.Insert(index, (T)e.NewItems[i]);
					}
 
					break;
 
				case NotifyCollectionChangedAction.Remove:
					if (e.OldStartingIndex < 0)
						goto case NotifyCollectionChangedAction.Reset;
 
					int removeIndex = e.OldStartingIndex;
					for (int i = removeIndex + e.OldItems.Count; i < Children.Count; i++)
						SetIndex((T)InternalChildren[i], removeIndex++);
 
					for (var i = 0; i < e.OldItems.Count; i++)
					{
						Element element = InternalChildren[e.OldStartingIndex];
						InternalChildren.RemoveAt(e.OldStartingIndex);
						element.Owned = false;
					}
 
					break;
 
				case NotifyCollectionChangedAction.Move:
					if (e.NewStartingIndex < 0 || e.OldStartingIndex < 0)
						goto case NotifyCollectionChangedAction.Reset;
 
					if (e.NewStartingIndex == e.OldStartingIndex)
						return;
 
					bool movingForward = e.OldStartingIndex < e.NewStartingIndex;
 
					if (movingForward)
					{
						int moveIndex = e.OldStartingIndex;
						for (int i = moveIndex + e.OldItems.Count; i <= e.NewStartingIndex; i++)
							SetIndex((T)InternalChildren[i], moveIndex++);
					}
					else
					{
						for (var i = 0; i < e.OldStartingIndex - e.NewStartingIndex; i++)
						{
							var page = (T)InternalChildren[i + e.NewStartingIndex];
							SetIndex(page, GetIndex(page) + e.OldItems.Count);
						}
					}
 
					for (var i = 0; i < e.OldItems.Count; i++)
						InternalChildren.RemoveAt(e.OldStartingIndex);
 
					int insertIndex = e.NewStartingIndex;
					if (movingForward)
						insertIndex -= e.OldItems.Count - 1;
 
					for (var i = 0; i < e.OldItems.Count; i++)
					{
						var page = (T)e.OldItems[i];
						SetIndex(page, insertIndex + i);
						InternalChildren.Insert(insertIndex + i, page);
					}
 
					break;
 
				case NotifyCollectionChangedAction.Replace:
					if (e.OldStartingIndex < 0)
						goto case NotifyCollectionChangedAction.Reset;
 
					for (int i = e.OldStartingIndex; i - e.OldStartingIndex < e.OldItems.Count; i++)
					{
						Element element = InternalChildren[i];
						InternalChildren.RemoveAt(i);
						element.Owned = false;
 
						T page = _templatedItems.GetOrCreateContent(i, e.NewItems[i - e.OldStartingIndex]);
						page.Owned = true;
						SetIndex(page, i);
						InternalChildren.Insert(i, page);
					}
 
					break;
 
				case NotifyCollectionChangedAction.Reset:
					Reset();
					return;
			}
 
			OnPagesChanged(e);
			UpdateCurrentPage();
		}
 
		void Reset()
		{
			List<Element> snapshot = InternalChildren.ToList();
 
			InternalChildren.Clear();
 
			foreach (Element element in snapshot)
				element.Owned = false;
 
			for (var i = 0; i < _templatedItems.Count; i++)
			{
				T page = _templatedItems.GetOrCreateContent(i, _templatedItems.ListProxy[i]);
				page.Owned = true;
				SetIndex(page, i);
				InternalChildren.Add(page);
			}
 
			var currentNeedsUpdate = true;
 
			BatchBegin();
 
			if (ItemsSource != null)
			{
				object selected = SelectedItem;
				if (selected == null || !ItemsSource.Cast<object>().Contains(selected))
				{
					SelectedItem = ItemsSource.Cast<object>().FirstOrDefault();
					currentNeedsUpdate = false;
				}
			}
 
			if (currentNeedsUpdate)
				UpdateCurrentPage();
 
			OnPagesChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
 
			BatchCommit();
		}
 
		void UpdateCurrentPage()
		{
			if (ItemsSource != null)
			{
				int index = _templatedItems.ListProxy.IndexOf(SelectedItem);
				if (index == -1)
					CurrentPage = (T)InternalChildren.FirstOrDefault();
				else
					CurrentPage = _templatedItems.GetOrCreateContent(index, SelectedItem);
			}
			else if (SelectedItem is T)
				CurrentPage = (T)SelectedItem;
		}
	}
}