File: Platform\ModalNavigationManager\ModalNavigationManager.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Controls.Internals;
 
namespace Microsoft.Maui.Controls.Platform
{
	internal partial class ModalNavigationManager
	{
		Window _window;
		public IReadOnlyList<Page> ModalStack => _modalPages.Pages;
		IMauiContext WindowMauiContext => _window.MauiContext;
 
		List<Page> _platformModalPages = new List<Page>();
		NavigatingStepRequestList _modalPages = new NavigatingStepRequestList();
 
		Page? _currentPage;
 
		Page CurrentPlatformPage =>
			_platformModalPages.Count > 0 ? _platformModalPages[_platformModalPages.Count - 1] : (_window.Page ?? throw new InvalidOperationException("Current Window isn't loaded"));
 
		Page CurrentPlatformModalPage =>
			_platformModalPages.Count > 0 ? _platformModalPages[_platformModalPages.Count - 1] : throw new InvalidOperationException("Modal Stack is Empty");
 
		Page? CurrentPage
		{
			get
			{
				var currentPage = _modalPages.Count > 0 ? _modalPages[_modalPages.Count - 1].Page : _window.Page;
 
				if (currentPage is Shell shell)
					currentPage = shell.CurrentPage;
 
				return currentPage;
			}
		}
 
		// Shell takes care of firing its own Modal life cycle events
		// With shell you cam remove / add multiple modals at once
		bool FireLifeCycleEvents => _window?.Page is not Shell;
 
		partial void InitializePlatform();
 
		public ModalNavigationManager(Window window)
		{
			_window = window;
			_window.PropertyChanged += (_, args) =>
			{
				if (args.Is(Window.PageProperty))
					SettingNewPage();
			};
 
			InitializePlatform();
 
			_window.HandlerChanging += OnWindowHandlerChanging;
			_window.Destroying += (_, _) =>
			{
				ClearModalPages(platform: true);
			};
		}
 
		void OnWindowHandlerChanging(object? sender, HandlerChangingEventArgs e)
		{
			// If the window handler is changing the activity is being recreated
			// the window activated/resumed event will take care of syncing the platform modals
			if (e.OldHandler is not null)
			{
				ClearModalPages(platform: true);
			}
		}
 
		public Task<Page?> PopModalAsync()
		{
			return PopModalAsync(true);
		}
 
		public Task PushModalAsync(Page modal)
		{
			return PushModalAsync(modal, true);
		}
 
		bool syncing = false;
 
		bool IsModalReady
		{
			get
			{
				return
					_window?.Page?.Handler is not null &&
					_window.Handler is not null
					&& IsModalPlatformReady;
			}
		}
 
		void SyncPlatformModalStack([CallerMemberName] string? callerName = null)
		{
			var logger = _window.FindMauiContext(true)?.Services?.CreateLogger<ModalNavigationManager>();
			SyncPlatformModalStackAsync().FireAndForget(logger, callerName);
		}
 
		void SyncModalStackWhenPlatformIsReady([CallerMemberName] string? callerName = null)
		{
			var logger = _window.FindMauiContext(true)?.Services?.CreateLogger<ModalNavigationManager>();
			SyncModalStackWhenPlatformIsReadyAsync().FireAndForget(logger, callerName);
		}
 
 
		// This code only processes a single sync action per call.
		// It recursively calls itself until no more sync actions are left to perform.
		//
		// A lot can change during the process of pushing/popping a page
		// i.e. Users might change the root page during an appearing event.
		// So, instead of just bull dozing through the whole sync we perform one
		// sync step then recalculate the state of affairs and then perform another
		// until no more sync operations are left.
		// Typically it's always a good idea to re-evaluate after any async operation has completed
		async Task SyncPlatformModalStackAsync()
		{
			if (!IsModalReady || syncing)
				return;
 
			bool syncAgain = false;
 
			try
			{
				syncing = true;
 
				int popTo;
 
				for (popTo = 0; popTo < _platformModalPages.Count && popTo < _modalPages.Count; popTo++)
				{
					if (_platformModalPages[popTo] != _modalPages[popTo].Page)
					{
						break;
					}
				}
 
				// This means the modal stacks are already synced so we don't have to do anything
				if (_platformModalPages.Count == _modalPages.Count && popTo == _platformModalPages.Count)
					return;
 
				// This ensures that appearing has fired on the final page that will be visible after 
				// the sync has finished
				CurrentPage?.SendAppearing();
 
				// Pop platform modal pages until we get to the point where the xplat expectation
				// matches the platform modals
				if (_platformModalPages.Count > popTo && IsModalReady)
				{
					bool animated = false;
					if (_modalPages.TryGetValue(CurrentPlatformModalPage, out var request))
					{
						_modalPages.Remove(CurrentPlatformModalPage);
						animated = request.IsAnimated;
					}
 
					var page = await PopModalPlatformAsync(animated);
					page.Parent?.RemoveLogicalChild(page);
					syncAgain = true;
				}
 
				if (!syncAgain)
				{
					//push any modals that need to be synced
					var i = _platformModalPages.Count;
					if (i < _modalPages.Count && IsModalReady)
					{
						var nextRequest = _modalPages[i];
						var nextPage = nextRequest.Page;
						bool animated = nextRequest.IsAnimated;
 
						await PushModalPlatformAsync(nextPage, animated);
						syncAgain = true;
					}
				}
			}
			finally
			{
				// Code has multiple exit points during the sync operation.
				// So we're using a try/finally to ensure that syncing always 
				// gets transitioned to false. If more exit points are added at a later point  
				// we don't have to always worry about the exit point setting syncing to false.
				syncing = false;
 
				// syncAgain is only set after a successful operation so we won't hit a case here
				// where we hit an infinite loop of syncing.
				if (syncAgain)
				{
					await SyncModalStackWhenPlatformIsReadyAsync().ConfigureAwait(false);
				}
			}
		}
 
		Task _waitForModalToFinishTask = Task.CompletedTask;
 
		public async Task<Page?> PopModalAsync(bool animated)
		{
			if (_modalPages.Count <= 0)
				throw new InvalidOperationException("PopModalAsync failed because modal stack is currently empty.");
 
			await _waitForModalToFinishTask;
 
			Page modal = _modalPages[_modalPages.Count - 1].Page;
 
			if (_window.OnModalPopping(modal))
			{
				_window.OnPopCanceled();
				return null;
			}
 
			_modalPages.Remove(modal);
 
			if (FireLifeCycleEvents)
			{
				modal.SendNavigatingFrom(new NavigatingFromEventArgs());
			}
 
			modal.SendDisappearing();
 
			// With shell we want to make sure to only fire the appearing event
			// on the final page that will be visible after the pop has completed
			if (_window.Page is Shell shell)
			{
				if (!shell.CurrentItem.CurrentItem.IsPoppingModalStack)
				{
					CurrentPage?.SendAppearing();
				}
			}
			else
			{
				CurrentPage?.SendAppearing();
			}
 
			bool isPlatformReady = IsModalReady;
			Task popTask =
				(isPlatformReady && !syncing) ? PopModalPlatformAsync(animated) : Task.CompletedTask;
 
			await popTask;
			modal.Parent?.RemoveLogicalChild(modal);
			_window.OnModalPopped(modal);
 
			if (FireLifeCycleEvents)
			{
				modal.SendNavigatedFrom(new NavigatedFromEventArgs(CurrentPage, NavigationType.Pop));
				CurrentPage?.SendNavigatedTo(new NavigatedToEventArgs(modal));
			}
 
			if (!isPlatformReady)
				SyncModalStackWhenPlatformIsReady();
 
			return modal;
		}
 
		public async Task PushModalAsync(Page modal, bool animated)
		{
			await _waitForModalToFinishTask;
 
			_window.OnModalPushing(modal);
 
			var previousPage = CurrentPage;
			_modalPages.Add(new NavigationStepRequest(modal, true, animated));
			_window.AddLogicalChild(modal);
 
			if (FireLifeCycleEvents)
			{
				previousPage?.SendNavigatingFrom(new NavigatingFromEventArgs());
			}
 
			if (_window.Page is Shell shell)
			{
				// With shell we want to make sure to only fire the appearing event
				// on the final page that will be visible after the pop has completed
				if (!shell.CurrentItem.CurrentItem.IsPushingModalStack)
				{
					previousPage?.SendDisappearing();
					CurrentPage?.SendAppearing();
				}
			}
			else
			{
				previousPage?.SendDisappearing();
				CurrentPage?.SendAppearing();
			}
 
			bool isPlatformReady = IsModalReady;
			if (isPlatformReady && !syncing)
			{
				if (ModalStack.Count == 0)
				{
					modal.NavigationProxy.Inner = _window.Navigation;
					await PushModalPlatformAsync(modal, animated);
				}
				else
				{
					await PushModalPlatformAsync(modal, animated);
					modal.NavigationProxy.Inner = _window.Navigation;
				}
			}
 
			if (FireLifeCycleEvents)
			{
				previousPage?.SendNavigatedFrom(new NavigatedFromEventArgs(CurrentPage, NavigationType.Push));
				CurrentPage?.SendNavigatedTo(new NavigatedToEventArgs(previousPage));
			}
 
			_window.OnModalPushed(modal);
 
			if (!isPlatformReady)
				SyncModalStackWhenPlatformIsReady();
		}
 
		void SettingNewPage()
		{
			if (_window.Page is null)
			{
				_currentPage = null;
				return;
			}
 
			if (_currentPage != _window.Page)
			{
				var previousPage = _currentPage;
				_currentPage = _window.Page;
 
				if (previousPage is not null)
				{
					previousPage.HandlerChanged -= OnCurrentPageHandlerChanged;
					ClearModalPages(xplat: true);
				}
 
				if (_currentPage is not null)
				{
					if (_currentPage.Handler is null)
					{
						_currentPage.HandlerChanged += OnCurrentPageHandlerChanged;
					}
					else
					{
						SyncModalStackWhenPlatformIsReady();
					}
				}
			}
		}
 
		void OnCurrentPageHandlerChanged(object? sender, EventArgs e)
		{
			if (_currentPage is not null)
			{
				_currentPage.HandlerChanged -= OnCurrentPageHandlerChanged;
				SyncModalStackWhenPlatformIsReady();
			}
		}
 
		partial void OnPageAttachedHandler();
 
		public void PageAttachedHandler() => OnPageAttachedHandler();
 
		void ClearModalPages(bool xplat = false, bool platform = false)
		{
			if (xplat)
				_modalPages.Clear();
 
			if (platform)
				_platformModalPages.Clear();
		}
 
		// Windows and Android have basically the same requirement that
		// we need to wait for the current page to finish loading before
		// satisfying Modal requests.
		// This will most likely change once we switch Android to using dialog fragments		
#if WINDOWS || ANDROID
		IDisposable? _platformPageWatchingForLoaded;
 
		async Task SyncModalStackWhenPlatformIsReadyAsync()
		{
			DisconnectPlatformPageWatchingForLoaded();
 
			if (IsModalPlatformReady)
			{
				await SyncPlatformModalStackAsync().ConfigureAwait(false);
			}
			else if (IsWindowReadyForModals)
			{
				if (CurrentPlatformPage.Handler is null)
				{
					CurrentPlatformPage.HandlerChanged += OnCurrentPlatformPageHandlerChanged;
 
					_platformPageWatchingForLoaded = new ActionDisposable(() =>
					{
						CurrentPlatformPage.HandlerChanged -= OnCurrentPlatformPageHandlerChanged;
					});
				}
				// This accounts for cases where we swap the root page out
				// We want to wait for that to finish loading before processing any modal changes
#if ANDROID
				else if (_window?.Page is not null && !_window.Page.IsLoadedOnPlatform())
				{
					var windowPage = _window.Page;
					_platformPageWatchingForLoaded =
						windowPage.OnLoaded(() => OnCurrentPlatformPageLoaded(windowPage, EventArgs.Empty));
				}
#endif

				if (!CurrentPlatformPage.IsLoadedOnPlatform() &&
						  CurrentPlatformPage.Handler is not null)
				{
					var currentPlatformPage = CurrentPlatformPage;
					_platformPageWatchingForLoaded =
						currentPlatformPage.OnLoaded(() => OnCurrentPlatformPageLoaded(currentPlatformPage, EventArgs.Empty));
				}
			}
		}
 
		void OnCurrentPlatformPageHandlerChanged(object? sender, EventArgs e)
		{
			DisconnectPlatformPageWatchingForLoaded();
			SyncModalStackWhenPlatformIsReady();
		}
 
		void DisconnectPlatformPageWatchingForLoaded()
		{
			_platformPageWatchingForLoaded?.Dispose();
		}
 
		void OnCurrentPlatformPageLoaded(object? sender, EventArgs e)
		{
			DisconnectPlatformPageWatchingForLoaded();
			SyncPlatformModalStack();
		}
 
		bool IsWindowReadyForModals =>
					_window?.Page?.Handler is not null &&
#if WINDOWS
					_firstActivated;
#else
					_window.IsActivated;
#endif

		bool IsModalPlatformReady
		{
			get
			{
				bool result =
					IsWindowReadyForModals
#if ANDROID
					&& _window?.Page?.IsLoadedOnPlatform() == true
#endif
					&& CurrentPlatformPage?.Handler is not null
					&& CurrentPlatformPage.IsLoadedOnPlatform();
 
				if (result)
					DisconnectPlatformPageWatchingForLoaded();
 
				return result;
			}
		}
#endif
	}
}