File: Shell\ShellSection.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Internals;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../../docs/Microsoft.Maui.Controls/Tab.xml" path="Type[@FullName='Microsoft.Maui.Controls.Tab']/Docs/*" />
	[EditorBrowsable(EditorBrowsableState.Always)]
	public class Tab : ShellSection
	{
	}
 
	/// <include file="../../../docs/Microsoft.Maui.Controls/ShellSection.xml" path="Type[@FullName='Microsoft.Maui.Controls.ShellSection']/Docs/*" />
	[ContentProperty(nameof(Items))]
	[EditorBrowsable(EditorBrowsableState.Never)]
	[TypeConverter(typeof(ShellSectionTypeConverter))]
	public partial class ShellSection : ShellGroupItem, IShellSectionController, IPropertyPropagationController, IVisualTreeElement, IStackNavigation
	{
		#region PropertyKeys
 
		static readonly BindablePropertyKey ItemsPropertyKey =
			BindableProperty.CreateReadOnly(nameof(Items), typeof(ShellContentCollection), typeof(ShellSection), null,
				defaultValueCreator: bo => new ShellContentCollection() { Inner = new ElementCollection<ShellContent>(((ShellSection)bo).DeclaredChildren) });
 
		#endregion PropertyKeys
 
		#region IShellSectionController
 
		IShellSectionController ShellSectionController => this;
		readonly List<(object Observer, Action<Page> Callback)> _displayedPageObservers =
			new List<(object Observer, Action<Page> Callback)>();
		readonly List<IShellContentInsetObserver> _observers = new List<IShellContentInsetObserver>();
		Thickness _lastInset;
		double _lastTabThickness;
 
		event EventHandler<NavigationRequestedEventArgs> IShellSectionController.NavigationRequested
		{
			add { _navigationRequested += value; }
			remove { _navigationRequested -= value; }
		}
 
		event EventHandler<NavigationRequestedEventArgs> _navigationRequested;
 
		Page IShellSectionController.PresentedPage
		{
			get
			{
				if (Navigation.ModalStack.Count > 0)
				{
					if (Navigation.ModalStack[Navigation.ModalStack.Count - 1] is NavigationPage np)
						return np.Navigation.NavigationStack[np.Navigation.NavigationStack.Count - 1];
 
					return Navigation.ModalStack[Navigation.ModalStack.Count - 1];
				}
 
				if (_navStack.Count > 1)
					return _navStack[_navStack.Count - 1];
 
				return ((IShellContentController)CurrentItem)?.Page;
			}
		}
 
		void IShellSectionController.AddContentInsetObserver(IShellContentInsetObserver observer)
		{
			if (!_observers.Contains(observer))
				_observers.Add(observer);
 
			observer.OnInsetChanged(_lastInset, _lastTabThickness);
		}
 
		void IShellSectionController.AddDisplayedPageObserver(object observer, Action<Page> callback)
		{
			_displayedPageObservers.Add((observer, callback));
			callback(DisplayedPage);
		}
 
		bool IShellSectionController.RemoveContentInsetObserver(IShellContentInsetObserver observer)
		{
			return _observers.Remove(observer);
		}
 
		bool IShellSectionController.RemoveDisplayedPageObserver(object observer)
		{
			foreach (var item in _displayedPageObservers)
			{
				if (item.Observer == observer)
				{
					return _displayedPageObservers.Remove(item);
				}
			}
			return false;
		}
 
		void IShellSectionController.SendInsetChanged(Thickness inset, double tabThickness)
		{
			foreach (var observer in _observers)
			{
				observer.OnInsetChanged(inset, tabThickness);
			}
			_lastInset = inset;
			_lastTabThickness = tabThickness;
		}
 
		async void IShellSectionController.SendPopping(Task poppingCompleted)
		{
			if (_navStack.Count <= 1)
				throw new Exception("Nav Stack consistency error");
 
			var page = _navStack[_navStack.Count - 1];
 
			_navStack.Remove(page);
			UpdateDisplayedPage();
 
			await poppingCompleted;
 
			RemovePage(page);
 
			(Parent?.Parent as IShellController)?.UpdateCurrentState(ShellNavigationSource.Pop);
		}
 
		async void IShellSectionController.SendPoppingToRoot(Task finishedPopping)
		{
			if (_navStack.Count <= 1)
				throw new Exception("Nav Stack consistency error");
 
			var oldStack = _navStack;
			_navStack = new List<Page> { null };
 
			for (int i = 1; i < oldStack.Count; i++)
				oldStack[i].SendDisappearing();
 
			UpdateDisplayedPage();
			await finishedPopping;
 
			for (int i = 1; i < oldStack.Count; i++)
				RemovePage(oldStack[i]);
 
			(Parent?.Parent as IShellController)?.UpdateCurrentState(ShellNavigationSource.PopToRoot);
		}
 
		[Obsolete]
		[EditorBrowsable(EditorBrowsableState.Never)]
 
		void IShellSectionController.SendPopped()
		{
			if (_navStack.Count <= 1)
				throw new Exception("Nav Stack consistency error");
 
			var last = _navStack[_navStack.Count - 1];
			_navStack.Remove(last);
 
			RemovePage(last);
		}
 
		// we want the list returned from here to remain point in time accurate
		ReadOnlyCollection<ShellContent> IShellSectionController.GetItems() => ((ShellContentCollection)Items).VisibleItemsReadOnly;
 
		[Obsolete]
		[EditorBrowsable(EditorBrowsableState.Never)]
		void IShellSectionController.SendPopping(Page page)
		{
			if (_navStack.Count <= 1)
				throw new Exception("Nav Stack consistency error");
 
			_navStack.Remove(page);
			SendAppearanceChanged();
		}
 
		[Obsolete]
		[EditorBrowsable(EditorBrowsableState.Never)]
		void IShellSectionController.SendPopped(Page page)
		{
			if (_navStack.Contains(page))
				_navStack.Remove(page);
 
			RemovePage(page);
		}
 
 
		event NotifyCollectionChangedEventHandler IShellSectionController.ItemsCollectionChanged
		{
			add { ((ShellContentCollection)Items).VisibleItemsChanged += value; }
			remove { ((ShellContentCollection)Items).VisibleItemsChanged -= value; }
		}
 
		#endregion IShellSectionController
 
		#region IPropertyPropagationController
		void IPropertyPropagationController.PropagatePropertyChanged(string propertyName)
		{
			PropertyPropagationExtensions.PropagatePropertyChanged(propertyName, this, ((IVisualTreeElement)this).GetVisualChildren());
		}
		#endregion
 
		/// <summary>Bindable property for <see cref="CurrentItem"/>.</summary>
		public static readonly BindableProperty CurrentItemProperty =
			BindableProperty.Create(nameof(CurrentItem), typeof(ShellContent), typeof(ShellSection), null, BindingMode.TwoWay,
				propertyChanged: OnCurrentItemChanged);
 
		/// <summary>Bindable property for <see cref="Items"/>.</summary>
		public static readonly BindableProperty ItemsProperty = ItemsPropertyKey.BindableProperty;
 
		Page _displayedPage;
		List<Page> _navStack = new List<Page> { null };
		internal bool IsPushingModalStack { get; private set; }
		internal bool IsPoppingModalStack { get; private set; }
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/ShellSection.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public ShellSection()
		{
			((ShellElementCollection)Items).VisibleItemsChangedInternal += (_, args) =>
			{
				if (args.OldItems != null)
				{
					foreach (Element item in args.OldItems)
					{
						OnVisibleChildRemoved(item);
					}
				}
 
				if (args.NewItems != null)
				{
					foreach (Element item in args.NewItems)
					{
						OnVisibleChildAdded(item);
					}
				}
 
				SendStructureChanged();
			};
 
			Navigation = new NavigationImpl(this);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/ShellSection.xml" path="//Member[@MemberName='CurrentItem']/Docs/*" />
		public ShellContent CurrentItem
		{
			get { return (ShellContent)GetValue(CurrentItemProperty); }
			set { SetValue(CurrentItemProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/ShellSection.xml" path="//Member[@MemberName='Items']/Docs/*" />
		public IList<ShellContent> Items => (IList<ShellContent>)GetValue(ItemsProperty);
		internal override ShellElementCollection ShellElementCollection => (ShellElementCollection)Items;
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/ShellSection.xml" path="//Member[@MemberName='Stack']/Docs/*" />
		public IReadOnlyList<Page> Stack => _navStack;
 
		internal Page DisplayedPage
		{
			get { return _displayedPage; }
			set
			{
				if (_displayedPage == value)
					return;
 
				_displayedPage = value;
 
				foreach (var item in _displayedPageObservers)
					item.Callback(_displayedPage);
			}
		}
 
		Shell Shell => Parent?.Parent as Shell;
 
		ShellItem ShellItem => Parent as ShellItem;
 
		internal static ShellSection CreateFromShellContent(ShellContent shellContent)
		{
			if (shellContent.Parent != null)
			{
				var current = (ShellSection)shellContent.Parent;
 
				if (current.Items.Contains(shellContent))
					current.CurrentItem = shellContent;
 
				return current;
			}
 
			var shellSection = new ShellSection();
 
			var contentRoute = shellContent.Route;
 
			shellSection.Route = Routing.GenerateImplicitRoute(contentRoute);
 
			shellSection.Items.Add(shellContent);
 
			shellSection.SetBinding(TitleProperty, static (BaseShellItem item) => item.Title, BindingMode.OneWay, source: shellContent);
			shellSection.SetBinding(IconProperty, static (BaseShellItem item) => item.Icon, BindingMode.OneWay, source: shellContent);
			shellSection.SetBinding(FlyoutIconProperty, static (BaseShellItem item) => item.FlyoutIcon, BindingMode.OneWay, source: shellContent);
 
			return shellSection;
		}
 
		internal static ShellSection CreateFromTemplatedPage(TemplatedPage page)
		{
			return CreateFromShellContent((ShellContent)page);
		}
 
		public static implicit operator ShellSection(ShellContent shellContent)
		{
			return CreateFromShellContent(shellContent);
		}
 
		public static implicit operator ShellSection(TemplatedPage page)
		{
			return (ShellSection)(ShellContent)page;
		}
 
		async Task PrepareCurrentStackForBeingReplaced(ShellNavigationRequest request, ShellRouteParameters queryData, IServiceProvider services, bool? animate, List<string> globalRoutes, bool isRelativePopping)
		{
			string route = "";
			List<Page> navStack = null;
 
			// Pop the stack down to where it no longer matches 
			if (request.StackRequest == ShellNavigationRequest.WhatToDoWithTheStack.ReplaceIt)
			{
				// If there's a visible Modal Stack then let's remove the pages under it that
				// are going to be popped so they never become visible and never fire OnAppearing
				if (Navigation.ModalStack.Count > 0)
				{
					var navStackCopy = new List<Page>(_navStack);
					for (int i = 1; i < navStackCopy.Count; i++)
					{
						var routeToRemove = Routing.GetRoute(navStackCopy[i]);
						if (i > globalRoutes.Count || routeToRemove != globalRoutes[i - 1])
						{
							OnRemovePage(navStackCopy[i]);
						}
					}
				}
 
				RemoveExcessPathsWithinTheRoute();
 
 
				// now that we've removed all the extra pages let's find the page on the stack that 
				// will be visible so we can push that as the first visible thing
				// If we already have a modal stack then we don't worry about doing this
				// Modal pages can't be selectively inserted/removed so this only matters when there are
				// no modal pages present
				if (Navigation.ModalStack.Count == 0)
				{
					List<Page> pagesToInsert = new List<Page>();
					for (int i = 0; i < globalRoutes.Count; i++)
					{
						bool isLast = i == globalRoutes.Count - 1;
						int navIndex = i + 1;
						// Routes match so don't do anything
						if (navIndex < _navStack.Count && Routing.GetRoute(_navStack[navIndex]) == globalRoutes[i])
						{
							continue;
						}
 
						var page = GetOrCreateFromRoute(globalRoutes[i], queryData, services, i == globalRoutes.Count - 1, false);
						if (IsModal(page))
						{
							await PushModalAsync(page, IsNavigationAnimated(page));
							break;
						}
						else if (!isLast && navIndex < _navStack.Count)
						{
							Navigation.InsertPageBefore(page, _navStack[navIndex]);
						}
						else
						{
							pagesToInsert.Add(page);
						}
					}
 
					await PushStackOfPages(pagesToInsert, animate);
					RemoveExcessPathsWithinTheRoute();
				}
 
				for (int i = 0; i < globalRoutes.Count; i++)
				{
					bool isLast = i == globalRoutes.Count - 1;
					route = globalRoutes[i];
 
					navStack = ShellNavigationManager.BuildFlattenedNavigationStack(_navStack, Navigation?.ModalStack);
 
					// if the navStack count is one that means there is nothing pushed
					if (navStack.Count == 1)
						break;
 
					Page navPage = navStack.Count > i + 1 ? navStack[i + 1] : null;
 
					if (navPage != null)
					{
						// if the routes don't match then pop this route off the stack
						int popCount = i + 1;
 
						if (Routing.GetRoute(navPage) == route)
						{
							// if the routes do match and this is the last in the loop
							// pop everything after this route
							popCount = i + 2;
							ShellNavigationManager.ApplyQueryAttributes(navPage, queryData, isLast, isRelativePopping);
 
							// If we're not on the last loop of the stack then continue
							// otherwise pop the rest of the stack
							if (!isLast)
								continue;
						}
 
						// This is the page that we will eventually get to once we've finished
						// modifying the existing navigation stack
						// So we want to fire appearing on it						
						navPage.SendAppearing();
 
						IsPoppingModalStack = true;
 
						while (navStack.Count > popCount && Navigation.ModalStack.Count > 0)
						{
							bool isAnimated = animate ?? IsNavigationAnimated(navStack[navStack.Count - 1]);
							if (Navigation.ModalStack.Contains(navStack[navStack.Count - 1]))
							{
								await PopModalAsync(isAnimated);
							}
							else if (Navigation.ModalStack.Count > 0)
							{
								await Navigation.ModalStack[Navigation.ModalStack.Count - 1].Navigation.PopAsync(isAnimated);
							}
 
							navStack = ShellNavigationManager.BuildFlattenedNavigationStack(_navStack, Navigation?.ModalStack);
						}
 
						while (_navStack.Count > popCount)
						{
							// Remove middle pages before doing a pop on the visible page so that the transition
							// is seamless
							if ((_navStack.Count - popCount) == 1)
							{
								bool isAnimated = animate ?? IsNavigationAnimated(_navStack[_navStack.Count - 1]);
								await OnPopAsync(isAnimated);
							}
							else
							{
								OnRemovePage(_navStack[_navStack.Count - 2]);
							}
						}
 
						navStack = ShellNavigationManager.BuildFlattenedNavigationStack(_navStack, Navigation?.ModalStack);
 
						IsPoppingModalStack = false;
 
						break;
					}
				}
			}
 
			void RemoveExcessPathsWithinTheRoute()
			{
				// locate middle routes that were removed
				// //route/page1/page2 => //route/page2
				// Let's just remove page1 instead of pop, pop, add
				for (int i = 0; i < globalRoutes.Count; i++)
				{
					int foundMatchAt = -1;
					for (int j = 1; j < _navStack.Count; j++)
					{
						if (Routing.GetRoute(_navStack[j]) == globalRoutes[i])
						{
							foundMatchAt = j;
							break;
						}
					}
 
					// If we found a matching route then let's remove all the middle pages
					for (int j = foundMatchAt - 1; j >= (i + 1); j--)
					{
						OnRemovePage(_navStack[j]);
					}
				}
			}
		}
 
		Page GetOrCreateFromRoute(string route, ShellRouteParameters queryData, IServiceProvider services, bool isLast, bool isPopping)
		{
			var content = Routing.GetOrCreateContent(route, services) as Page;
			if (content == null)
			{
				Application.Current?.FindMauiContext()?.CreateLogger<ShellSection>()?.LogWarning("Failed to Create Content For: {route}", route);
			}
 
			ShellNavigationManager.ApplyQueryAttributes(content, queryData, isLast, isPopping);
			return content;
		}
 
		internal async Task GoToAsync(ShellNavigationRequest request, ShellRouteParameters queryData, IServiceProvider services, bool? animate, bool isRelativePopping)
		{
			List<string> globalRoutes = request.Request.GlobalRoutes;
			if (globalRoutes == null || globalRoutes.Count == 0)
			{
				if (_navStack.Count == 2)
					await OnPopAsync(animate ?? false);
				else
					await OnPopToRootAsync(animate ?? false);
 
				return;
			}
 
			await PrepareCurrentStackForBeingReplaced(request, queryData, services, animate, globalRoutes, isRelativePopping);
 
			List<Page> modalPageStacks = new List<Page>();
			List<Page> nonModalPageStacks = new List<Page>();
			var currentNavStack = ShellNavigationManager.BuildFlattenedNavigationStack(_navStack, Navigation?.ModalStack);
 
			// populate global routes and build modal stacks
 
			// If the currentNavStack is larger than _navStack then we have modal pages
			bool weveGoneTotalModal = currentNavStack.Count > _navStack.Count;
			int whereToStartNavigation = 0;
 
			if (request.StackRequest == ShellNavigationRequest.WhatToDoWithTheStack.ReplaceIt)
				whereToStartNavigation = currentNavStack.Count - 1;
 
			for (int i = whereToStartNavigation; i < globalRoutes.Count; i++)
			{
				bool isLast = i == globalRoutes.Count - 1;
				var content = GetOrCreateFromRoute(globalRoutes[i], queryData, services, isLast, false);
				if (content == null)
				{
					break;
				}
 
				weveGoneTotalModal = weveGoneTotalModal || IsModal(content);
 
				if (weveGoneTotalModal)
				{
					modalPageStacks.Add(content);
				}
				else
				{
					nonModalPageStacks.Add(content);
				}
			}
 
			// Check if we have an active Navigation Page
			NavigationPage activeModalNavigationPage = null;
			for (int i = Navigation.ModalStack.Count - 1; i >= 0; i--)
			{
				if (Navigation.ModalStack[i] is NavigationPage np)
				{
					activeModalNavigationPage = np;
					break;
				}
			}
 
			for (int i = 0; i < modalPageStacks.Count; i++)
			{
				bool isLast = i == modalPageStacks.Count - 1;
				var modalPage = modalPageStacks[i];
				bool isAnimated = animate ?? IsNavigationAnimated(modalPage);
				IsPushingModalStack = !isLast;
 
				if (modalPage is NavigationPage np)
				{
					await PushModalAsync(modalPage, isAnimated);
					activeModalNavigationPage = np;
				}
				else
				{
					if (activeModalNavigationPage != null)
						await activeModalNavigationPage.Navigation.PushAsync(modalPage, animate ?? IsNavigationAnimated(modalPage));
					else
						await PushModalAsync(modalPage, isAnimated);
				}
			}
 
			await PushStackOfPages(nonModalPageStacks, animate);
		}
 
		Task PopModalAsync(bool isAnimated)
		{
			if (Navigation is NavigationImpl shellSectionProxy)
				return shellSectionProxy.PopModalInnerAsync(isAnimated);
 
			return Navigation.PopModalAsync(isAnimated);
		}
 
		Task PushModalAsync(Page page, bool isAnimated)
		{
			if (Navigation is NavigationImpl shellSectionProxy)
				return shellSectionProxy.PushModalInnerAsync(page, isAnimated);
 
			return Navigation.PushModalAsync(page, isAnimated);
		}
 
		async Task PushStackOfPages(List<Page> pages, bool? animate)
		{
			for (int i = pages.Count - 1; i >= 0; i--)
			{
				bool isLast = i == pages.Count - 1;
 
				if (isLast)
				{
					bool isAnimated = animate ?? IsNavigationAnimated(pages[i]);
					await OnPushAsync(pages[i], isAnimated);
				}
				else
					Navigation.InsertPageBefore(pages[i], pages[i + 1]);
			}
		}
 
		bool IsModal(BindableObject bo)
		{
			return (Shell.GetPresentationMode(bo) & PresentationMode.Modal) == PresentationMode.Modal;
		}
 
		bool IsNavigationAnimated(BindableObject bo)
		{
			return (Shell.GetPresentationMode(bo) & PresentationMode.NotAnimated) != PresentationMode.NotAnimated;
		}
 
		internal void SendStructureChanged()
		{
			if (Parent?.Parent is Shell shell)
			{
				if (IsVisibleSection)
					shell.SendStructureChanged();
 
				shell.SendFlyoutItemsChanged();
			}
		}
 
		protected virtual IReadOnlyList<Page> GetNavigationStack() => _navStack;
 
		internal void UpdateDisplayedPage()
		{
			var stack = Stack;
			var previousPage = DisplayedPage;
			if (stack.Count > 1)
			{
				DisplayedPage = stack[stack.Count - 1];
			}
			else
			{
				IShellContentController currentItem = CurrentItem;
				DisplayedPage = currentItem?.Page;
			}
 
			if (previousPage != DisplayedPage)
			{
				previousPage?.SendDisappearing();
				if (!Navigation.ModalStack.Any())
				{
					PresentedPageAppearing();
					SendAppearanceChanged();
				}
			}
		}
 
		protected override void OnParentSet()
		{
			base.OnParentSet();
 
			if (this.IsVisibleSection)
				SendAppearanceChanged();
		}
 
		protected override void OnChildAdded(Element child)
		{
			base.OnChildAdded(child);
			OnVisibleChildAdded(child);
		}
 
		protected override void OnChildRemoved(Element child, int oldLogicalIndex)
		{
			if (child is IShellContentController sc && (sc.Page?.IsPlatformEnabled == true))
			{
				sc.Page.PlatformEnabledChanged += WaitForRendererToGetRemoved;
				void WaitForRendererToGetRemoved(object s, EventArgs p)
				{
					sc.Page.PlatformEnabledChanged -= WaitForRendererToGetRemoved;
					base.OnChildRemoved(child, oldLogicalIndex);
				};
			}
			else
			{
				base.OnChildRemoved(child, oldLogicalIndex);
			}
 
			OnVisibleChildRemoved(child);
		}
 
		void OnVisibleChildAdded(Element child)
		{
			if (CurrentItem == null && ((IShellSectionController)this).GetItems().Contains(child))
				SetValueFromRenderer(CurrentItemProperty, child);
 
			if (CurrentItem != null)
				UpdateDisplayedPage();
		}
 
		void OnVisibleChildRemoved(Element child)
		{
			if (CurrentItem == child)
			{
				var contentItems = ShellSectionController.GetItems();
				if (contentItems.Count == 0)
				{
					ClearValue(CurrentItemProperty, specificity: SetterSpecificity.FromHandler);
				}
				else
				{
					SetValueFromRenderer(CurrentItemProperty, contentItems[0]);
				}
			}
 
			UpdateDisplayedPage();
		}
 
		void InvokeNavigationRequest(NavigationRequestedEventArgs args)
		{
			_navigationRequested?.Invoke(this, args);
		}
 
		protected virtual void OnInsertPageBefore(Page page, Page before)
		{
			var index = _navStack.IndexOf(before);
			if (index == -1)
				throw new ArgumentException("Page not found in nav stack");
 
			var stack = _navStack.ToList();
			stack.Insert(index, page);
 
			var allow = ((IShellController)Shell).ProposeNavigation(
				ShellNavigationSource.Insert,
				Parent as ShellItem,
				this,
				CurrentItem,
				stack,
				true
			);
 
			if (!allow)
				return;
 
			_navStack.Insert(index, page);
			AddPage(page);
 
			var args = new NavigationRequestedEventArgs(page, before, false)
			{
				RequestType = NavigationRequestType.Insert
			};
 
			InvokeNavigationRequest(args);
		}
 
		protected async virtual Task<Page> OnPopAsync(bool animated)
		{
			if (_navStack.Count <= 1)
				throw new InvalidOperationException("Can't pop last page off stack");
 
			List<Page> stack = _navStack.ToList();
			stack.Remove(stack.Last());
			var allow = ((IShellController)Shell).ProposeNavigation(
				ShellNavigationSource.Pop,
				Parent as ShellItem,
				this,
				CurrentItem,
				stack,
				true
			);
 
			if (!allow)
				return null;
 
			var page = _navStack[_navStack.Count - 1];
			var args = new NavigationRequestedEventArgs(page, animated)
			{
				RequestType = NavigationRequestType.Pop
			};
 
			PresentedPageDisappearing();
			_navStack.Remove(page);
			PresentedPageAppearing();
 
			InvokeNavigationRequest(args);
			if (args.Task != null)
				await args.Task;
 
			if (_handlerBasedNavigationCompletionSource?.Task != null)
				await _handlerBasedNavigationCompletionSource.Task;
 
			RemovePage(page);
 
			return page;
		}
 
		protected virtual async Task OnPopToRootAsync(bool animated)
		{
			if (_navStack.Count <= 1)
				return;
 
			var allow = ((IShellController)Shell).ProposeNavigation(
				ShellNavigationSource.PopToRoot,
				Parent as ShellItem,
				this,
				CurrentItem,
				null,
				true
			);
 
			if (!allow)
				return;
 
			var page = _navStack[_navStack.Count - 1];
			var args = new NavigationRequestedEventArgs(page, animated)
			{
				RequestType = NavigationRequestType.PopToRoot
			};
 
			InvokeNavigationRequest(args);
			var oldStack = _navStack;
			_navStack = new List<Page> { null };
 
			if (args.Task != null)
				await args.Task;
 
			if (_handlerBasedNavigationCompletionSource?.Task != null)
				await _handlerBasedNavigationCompletionSource.Task;
 
			for (int i = 1; i < oldStack.Count; i++)
			{
				oldStack[i].SendDisappearing();
				RemovePage(oldStack[i]);
			}
 
			PresentedPageAppearing();
		}
 
		protected virtual Task OnPushAsync(Page page, bool animated)
		{
			List<Page> stack = _navStack.ToList();
			stack.Add(page);
			var allow = ((IShellController)Shell).ProposeNavigation(
				ShellNavigationSource.Push,
				ShellItem,
				this,
				CurrentItem,
				stack,
				true
			);
 
			if (!allow)
				return Task.FromResult(true);
 
			var args = new NavigationRequestedEventArgs(page, animated)
			{
				RequestType = NavigationRequestType.Push
			};
 
			PresentedPageDisappearing();
			_navStack.Add(page);
			PresentedPageAppearing();
			AddPage(page);
			InvokeNavigationRequest(args);
 
			return args.Task ??
				_handlerBasedNavigationCompletionSource?.Task ??
				Task.CompletedTask;
		}
 
		internal async Task PopModalStackToPage(Page page, bool? animated)
		{
			try
			{
				IsPoppingModalStack = true;
				int modalStackCount = Navigation.ModalStack.Count;
				for (int i = 0; i < modalStackCount; i++)
				{
					var pageToPop = Navigation.ModalStack[Navigation.ModalStack.Count - 1];
					if (pageToPop == page)
						break;
 
					// indicate that we are done popping down the stack to the modal page requested
					// This is mainly used by life cycle events so they don't fire onappearing
					if (page == null && Navigation.ModalStack.Count == 1)
					{
						IsPoppingModalStack = false;
					}
					else if (Navigation.ModalStack.Count > 1 && Navigation.ModalStack[Navigation.ModalStack.Count - 2] == page)
					{
						IsPoppingModalStack = false;
					}
 
					bool isAnimated = animated ?? (Shell.GetPresentationMode(pageToPop) & PresentationMode.NotAnimated) != PresentationMode.NotAnimated;
					await PopModalAsync(isAnimated);
				}
			}
			finally
			{
				IsPoppingModalStack = false;
			}
		}
 
		protected virtual void OnRemovePage(Page page)
		{
			if (!_navStack.Contains(page))
				return;
 
			bool currentPage = (((IShellSectionController)this).PresentedPage) == page;
			var stack = _navStack.ToList();
			stack.Remove(page);
			var allow = (!currentPage) ? true :
				((IShellController)Shell).ProposeNavigation(
					ShellNavigationSource.Remove,
					ShellItem,
					this,
					CurrentItem,
					stack,
					true
				);
 
			if (!allow)
				return;
 
			if (currentPage)
				PresentedPageDisappearing();
 
			_navStack.Remove(page);
 
			if (currentPage)
				PresentedPageAppearing();
 
			RemovePage(page);
			var args = new NavigationRequestedEventArgs(page, false)
			{
				RequestType = NavigationRequestType.Remove
			};
			InvokeNavigationRequest(args);
		}
 
		internal bool IsVisibleSection => Parent?.Parent is Shell shell && shell.CurrentItem?.CurrentItem == this;
		void PresentedPageDisappearing()
		{
			if (this is IShellSectionController sectionController)
			{
				CurrentItem?.SendDisappearing();
				sectionController.PresentedPage?.SendDisappearing();
			}
		}
 
		void PresentedPageAppearing()
		{
			if (IsVisibleSection && this is IShellSectionController sectionController)
			{
				if (_navStack.Count == 1)
					CurrentItem?.SendAppearing();
 
				var presentedPage = sectionController.PresentedPage;
				if (presentedPage != null)
				{
					if (presentedPage.Parent == null)
					{
						presentedPage.ParentSet += OnPresentedPageParentSet;
 
						void OnPresentedPageParentSet(object sender, EventArgs e)
						{
							PresentedPageAppearing();
							(sender as Page).ParentSet -= OnPresentedPageParentSet;
						}
					}
					else
					{
 
						this.FindParentOfType<Shell>().SendPageAppearing(presentedPage);
					}
				}
			}
		}
 
		static void OnCurrentItemChanged(BindableObject bindable, object oldValue, object newValue)
		{
			var shellSection = (ShellSection)bindable;
 
			if (oldValue is ShellContent oldShellItem)
				oldShellItem.SendDisappearing();
 
			if (newValue == null)
				return;
 
			shellSection.PresentedPageAppearing();
 
			if (shellSection.Parent?.Parent is IShellController shell && shellSection.IsVisibleSection)
			{
				shell.UpdateCurrentState(ShellNavigationSource.ShellContentChanged);
			}
 
			shellSection.SendStructureChanged();
 
			if (shellSection.IsVisibleSection)
				((IShellController)shellSection?.Parent?.Parent)?.AppearanceChanged(shellSection, false);
 
			shellSection.UpdateDisplayedPage();
		}
 
		void AddPage(Page page)
		{
			AddLogicalChild(page);
		}
 
		void RemovePage(Page page)
		{
			RemoveLogicalChild(page);
		}
 
		void SendAppearanceChanged() => ((IShellController)Parent?.Parent)?.AppearanceChanged(this, false);
 
		protected override void OnBindingContextChanged()
		{
			base.OnBindingContextChanged();
 
			foreach (ShellContent shellContent in Items)
			{
				SetInheritedBindingContext(shellContent, BindingContext);
			}
		}
 
		internal override void SendDisappearing()
		{
			base.SendDisappearing();
			PresentedPageDisappearing();
		}
 
		internal override void SendAppearing()
		{
			base.SendAppearing();
			PresentedPageAppearing();
		}
 
		class NavigationImpl : NavigationProxy
		{
			readonly ShellSection _owner;
 
			public NavigationImpl(ShellSection owner) => _owner = owner;
 
			protected override IReadOnlyList<Page> GetNavigationStack() => _owner.GetNavigationStack();
 
			protected override async Task<Page> OnPopAsync(bool animated)
			{
				if (!_owner.IsVisibleSection)
				{
					return (await _owner.OnPopAsync(animated));
				}
 
				var navigationParameters = new ShellNavigationParameters()
				{
					Animated = animated,
					TargetState = new ShellNavigationState("..")
				};
 
				var returnedPage = (_owner as IShellSectionController).PresentedPage;
				await _owner.Shell.NavigationManager.GoToAsync(navigationParameters);
 
				// This means the page wasn't popped and navigation was cancelled
				if ((_owner as IShellSectionController).PresentedPage == returnedPage)
					return null;
 
				return returnedPage;
			}
 
			protected override Task OnPopToRootAsync(bool animated)
			{
				if (!_owner.IsVisibleSection)
				{
					return _owner.OnPopToRootAsync(animated);
				}
 
				var shell = _owner.Shell;
				var targetState =
					ShellNavigationManager.GetNavigationState(
						shell.CurrentItem,
						_owner,
						_owner.CurrentItem,
						null,
						null);
 
				var navigationParameters = new ShellNavigationParameters()
				{
					Animated = animated,
					TargetState = targetState,
					PopAllPagesNotSpecifiedOnTargetState = true
				};
 
				return _owner.Shell.NavigationManager.GoToAsync(navigationParameters);
			}
 
			protected override Task OnPushAsync(Page page, bool animated)
			{
				if (!_owner.IsVisibleSection)
					return _owner.OnPushAsync(page, animated);
 
				var navigationParameters = new ShellNavigationParameters()
				{
					Animated = animated,
					PagePushing = page
				};
 
				return _owner.Shell.NavigationManager.GoToAsync(navigationParameters);
			}
 
			// This is used when we just want to process the modal operation and we don't need
			// it to process through the internal shell navigation bits
			internal Task PushModalInnerAsync(Page modal, bool animated)
			{
				return Inner?.PushModalAsync(modal, animated);
			}
 
			// This is used when we just want to process the modal operation and we don't need
			// it to process through the internal shell navigation bits
			internal Task<Page> PopModalInnerAsync(bool animated)
			{
				return Inner?.PopModalAsync(animated);
			}
 
			protected override async Task OnPushModal(Page modal, bool animated)
			{
				if (_owner.Shell is null ||
					_owner.Shell.NavigationManager.AccumulateNavigatedEvents)
				{
					await base.OnPushModal(modal, animated);
					return;
				}
 
				if (animated)
					Shell.SetPresentationMode(modal, PresentationMode.ModalAnimated);
				else
					Shell.SetPresentationMode(modal, PresentationMode.ModalNotAnimated);
 
				var navigationParameters = new ShellNavigationParameters()
				{
					Animated = animated,
					PagePushing = modal
				};
 
				await _owner.Shell.NavigationManager.GoToAsync(navigationParameters);
			}
 
			protected async override Task<Page> OnPopModal(bool animated)
			{
				if (_owner.Shell.NavigationManager.AccumulateNavigatedEvents)
					return await base.OnPopModal(animated);
 
				var page = ModalStack[ModalStack.Count - 1];
				await _owner.Shell.GoToAsync("..", animated);
				return page;
			}
 
			protected override void OnRemovePage(Page page)
			{
				if (!_owner.IsVisibleSection || _owner.Shell.NavigationManager.AccumulateNavigatedEvents)
				{
					_owner.OnRemovePage(page);
					return;
				}
 
				var stack = _owner.Stack.ToList();
				stack.Remove(page);
				var navigationState = GetUpdatedStatus(stack);
 
				ShellNavigatingEventArgs shellNavigatingEventArgs =
					new ShellNavigatingEventArgs(
						_owner.Shell.CurrentState,
						navigationState.Location,
						ShellNavigationSource.Remove,
						false
					);
 
				_owner.Shell.NavigationManager.HandleNavigating(shellNavigatingEventArgs);
				_owner.OnRemovePage(page);
				(_owner.Shell as IShellController).UpdateCurrentState(ShellNavigationSource.Remove);
			}
 
			protected override void OnInsertPageBefore(Page page, Page before)
			{
				if (!_owner.IsVisibleSection || _owner.Shell.NavigationManager.AccumulateNavigatedEvents)
				{
					_owner.OnInsertPageBefore(page, before);
					return;
				}
 
				var stack = _owner.Stack.ToList();
				var index = stack.IndexOf(before);
				if (index == -1)
					throw new ArgumentException("Page not found in nav stack");
 
				stack.Insert(index, page);
				var navigationState = GetUpdatedStatus(stack);
 
				ShellNavigatingEventArgs shellNavigatingEventArgs =
					new ShellNavigatingEventArgs(
						_owner.Shell.CurrentState,
						navigationState.Location,
						ShellNavigationSource.Insert,
						false
					);
 
				_owner.Shell.NavigationManager.HandleNavigating(shellNavigatingEventArgs);
				_owner.OnInsertPageBefore(page, before);
				(_owner.Shell as IShellController).UpdateCurrentState(ShellNavigationSource.Insert);
			}
 
			ShellNavigationState GetUpdatedStatus(IReadOnlyList<Page> stack)
			{
				var shellItem = _owner.Shell.CurrentItem;
				var shellSection = shellItem?.CurrentItem;
				var shellContent = shellSection?.CurrentItem;
				var modalStack = shellSection?.Navigation?.ModalStack;
				return ShellNavigationManager.GetNavigationState(shellItem, shellSection, shellContent, stack, modalStack);
			}
		}
 
#nullable enable
		// This code only runs for shell bits that are running through a proper
		// ShellHandler
		TaskCompletionSource<object>? _handlerBasedNavigationCompletionSource;
		internal Task? PendingNavigationTask => _handlerBasedNavigationCompletionSource?.Task;
 
		void IStackNavigation.RequestNavigation(NavigationRequest eventArgs)
		{
			if (_handlerBasedNavigationCompletionSource != null)
				throw new InvalidOperationException("Pending Navigations still processing");
 
			_handlerBasedNavigationCompletionSource = new TaskCompletionSource<object>();
			Handler.Invoke(nameof(IStackNavigation.RequestNavigation), eventArgs);
		}
 
		void IStackNavigation.NavigationFinished(IReadOnlyList<IView> newStack)
		{
			_ = _handlerBasedNavigationCompletionSource ?? throw new InvalidOperationException("Mismatched Navigation finished");
			var source = _handlerBasedNavigationCompletionSource;
			_handlerBasedNavigationCompletionSource = null;
			source?.SetResult(true);
		}
#nullable disable
 
		private sealed class ShellSectionTypeConverter : TypeConverter
		{
			public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) => false;
			public override object ConvertTo(ITypeDescriptorContext context, CultureInfo cultureInfo, object value, Type destinationType) => throw new NotSupportedException();
 
			public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
				=> sourceType == typeof(ShellContent) || sourceType == typeof(TemplatedPage);
			public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
				=> value switch
				{
					ShellContent shellContent => (ShellSection)shellContent,
					TemplatedPage page => (ShellSection)page,
					_ => throw new NotSupportedException(),
				};
		}
	}
}