| 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 } }