Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix initial page and back button Navigation events #12348

Merged
merged 11 commits into from
Feb 6, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Threading.Tasks;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
Expand Down Expand Up @@ -42,8 +43,9 @@ public class NavigationRenderer : UINavigationController, IPlatformViewHandler
ViewHandlerDelegator<NavigationPage> _viewHandlerWrapper;
bool _navigating = false;
VisualElement _element;
bool _uiRequestedPop; // User tapped the back button or swiped to navigate back

[Preserve(Conditional = true)]
[Internals.Preserve(Conditional = true)]
public NavigationRenderer() : base(typeof(MauiControlsNavigationBar), null)
{
_viewHandlerWrapper = new ViewHandlerDelegator<NavigationPage>(Mapper, CommandMapper, this);
Expand Down Expand Up @@ -780,7 +782,7 @@ void UpdateToolBarVisible()
TopViewController?.NavigationItem?.TitleView?.LayoutSubviews();
}

internal async Task UpdateFormsInnerNavigation(Page pageBeingRemoved)
async Task UpdateFormsInnerNavigation(Page pageBeingRemoved)
{
if (NavPage == null)
return;
Expand All @@ -789,9 +791,24 @@ internal async Task UpdateFormsInnerNavigation(Page pageBeingRemoved)

_ignorePopCall = true;
if (Element.Navigation.NavigationStack.Contains(pageBeingRemoved))
{
await (NavPage as INavigationPageController)?.RemoveAsyncInner(pageBeingRemoved, false, true);
if (_uiRequestedPop)
{
NavPage?.SendNavigatedFromHandler(pageBeingRemoved);
}
}

_ignorePopCall = false;
_uiRequestedPop = false;
}

[Export("navigationBar:shouldPopItem:")]
[Internals.Preserve(Conditional = true)]
internal bool ShouldPopItem(UINavigationBar _, UINavigationItem __)
{
_uiRequestedPop = true;
return true;
}

internal static void SetFlyoutLeftBarButton(UIViewController containerController, FlyoutPage FlyoutPage)
Expand Down Expand Up @@ -959,7 +976,9 @@ void SetupLines()

class MauiNavigationDelegate : UINavigationControllerDelegate
{
bool _finishedWithInitialNavigation;
readonly WeakReference<NavigationRenderer> _navigation;

public MauiNavigationDelegate(NavigationRenderer navigationRenderer)
{
_navigation = new WeakReference<NavigationRenderer>(navigationRenderer);
Expand All @@ -974,6 +993,12 @@ public override void DidShowViewController(UINavigationController navigationCont
{
pvc.UpdateFrames();
}

if (r.Element is NavigationPage np && !_finishedWithInitialNavigation)
{
_finishedWithInitialNavigation = true;
np.SendNavigatedFromHandler(null);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ namespace Microsoft.Maui.Controls
/// <include file="../../../../docs/Microsoft.Maui.Controls/NavigationPage.xml" path="Type[@FullName='Microsoft.Maui.Controls.NavigationPage']/Docs/*" />
public partial class NavigationPage : INavigationPageController
{
internal async Task<Page> PopAsyncInner(
async Task<Page> PopAsyncInner(
bool animated,
bool fast,
bool requestedFromHandler)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the code was just passing false to requestedFromHandler so I removed it.

bool fast)
{
if (NavigationPageController.StackDepth == 1)
{
Expand All @@ -27,16 +26,15 @@ internal async Task<Page> PopAsyncInner(
var page = (Page)InternalChildren.Last();
var previousPage = CurrentPage;
SendNavigating();
var removedPage = await RemoveAsyncInner(page, animated, fast, requestedFromHandler);
var removedPage = await RemoveAsyncInner(page, animated, fast);
SendNavigated(previousPage);
return removedPage;
}

internal async Task<Page> RemoveAsyncInner(
async Task<Page> RemoveAsyncInner(
Page page,
bool animated,
bool fast,
bool requestedFromHandler)
bool fast)
{
if (NavigationPageController.StackDepth == 1)
{
Expand All @@ -53,7 +51,7 @@ internal async Task<Page> RemoveAsyncInner(
var removed = true;

EventHandler<NavigationRequestedEventArgs> requestPop = _popRequested;
if (requestPop != null && !requestedFromHandler)
if (requestPop != null)
{
requestPop(this, args);

Expand All @@ -74,15 +72,14 @@ internal async Task<Page> RemoveAsyncInner(
return page;
}


Task<Page> INavigationPageController.PopAsyncInner(bool animated, bool fast)
{
return PopAsyncInner(animated, fast, false);
return PopAsyncInner(animated, fast);
}

Task<Page> INavigationPageController.RemoveAsyncInner(Page page, bool animated, bool fast)
{
return RemoveAsyncInner(page, animated, fast, false);
return RemoveAsyncInner(page, animated, fast);
}

EventHandler<NavigationRequestedEventArgs> _popRequested;
Expand Down Expand Up @@ -207,6 +204,20 @@ async Task PushAsyncInner(Page page, bool animated)
Pushed?.Invoke(this, args);
}

#if IOS
// Because iOS currently doesn't use our `IStackNavigationView` structures
// there are scenarios where the legacy handler needs to alert the xplat
// code of when a navigation has occurred.
// For example, initial load and when the user taps the back button
internal void SendNavigatedFromHandler(Page previousPage)
{
if (CurrentPage.HasNavigatedTo)
return;

SendNavigated(previousPage);
}
#endif

void PushPage(Page page)
{
InternalChildren.Add(page);
Expand Down
7 changes: 0 additions & 7 deletions src/Controls/src/Core/NavigationPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,13 +377,6 @@ protected override bool OnBackButtonPressed()
return base.OnBackButtonPressed();
}

internal void InitialNativeNavigationStackLoaded()
{
SendNavigated(null);
}



void SendNavigated(Page previousPage)
{
previousPage?.SendNavigatedFrom(new NavigatedFromEventArgs(CurrentPage));
Expand Down
16 changes: 10 additions & 6 deletions src/Controls/tests/Core.UnitTests/PageLifeCycleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ namespace Microsoft.Maui.Controls.Core.UnitTests

public class PageLifeCycleTests : BaseTestFixture
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void NavigationPageInitialPage(bool useMaui)
[Fact]
// This test isn't valid for non handler based
// navigation because the initial navigated event
// fires from the legacy code instead of the
// new handler code
// We have device tests to also verify this works on
// each platform
public async Task NavigationPageInitialPage()
{
var lcPage = new LCPage();
NavigationPage navigationPage = new TestNavigationPage(useMaui, lcPage);
navigationPage.InitialNativeNavigationStackLoaded();
var navigationPage = new TestNavigationPage(true, lcPage);
await navigationPage.NavigatingTask;
Assert.Null(lcPage.NavigatingFromArgs);
Assert.Null(lcPage.NavigatedFromArgs);
Assert.NotNull(lcPage.NavigatedToArgs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public async Task<bool> SendBackButtonPressedAsync()
return result;
}


public Task NavigatingTask => Handler?.NavigatingTask ?? Task.CompletedTask;
}

public class TestNavigationHandler : ViewHandler<NavigationPage, object>
Expand All @@ -54,24 +54,36 @@ public static PropertyMapper<IStackNavigationView, TestNavigationHandler> Naviga

public NavigationRequest CurrentNavigationRequest { get; private set; }

TaskCompletionSource _navigationSource;

public Task NavigatingTask => (_navigationSource?.Task ?? Task.CompletedTask);

public void CompleteCurrentNavigation()
public async void CompleteCurrentNavigation()
{
if (CurrentNavigationRequest == null)
throw new InvalidOperationException("No Active Navigation in the works");

var newStack = CurrentNavigationRequest.NavigationStack.ToList();
CurrentNavigationRequest = null;

var source = _navigationSource;
_navigationSource = null;

if ((this as IElementHandler).VirtualView is IStackNavigation sn)
sn.NavigationFinished(newStack);


await Task.Delay(1);
source.SetResult();
}

async void RequestNavigation(NavigationRequest navigationRequest)
{
if (CurrentNavigationRequest != null)
if (CurrentNavigationRequest != null || _navigationSource != null)
throw new InvalidOperationException("Already Processing Navigation");


_navigationSource = new TaskCompletionSource();
CurrentNavigationRequest = navigationRequest;

await Task.Delay(10);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,43 @@ await CreateHandlerAndAddToWindow<WindowHandlerStub>(new Window(navPage), async
});
}

[Fact]
public async Task InitialPageFiresNavigatedEvent()
{
SetupBuilder();
var page = new ContentPage();
var navPage = new NavigationPage(page) { Title = "App Page" };

await CreateHandlerAndAddToWindow<WindowHandlerStub>(new Window(navPage), async (handler) =>
{
await OnNavigatedToAsync(page);
Assert.True(page.HasNavigatedTo);
});
}

[Fact]
public async Task PushedPageFiresNavigatedEventOnInitialLoad()
{
SetupBuilder();

bool pageFiredNavigated = false;
var page = new ContentPage();
page.NavigatedTo += (_, _) => pageFiredNavigated = true;

var page2 = new ContentPage();

var navPage = new NavigationPage(page) { Title = "App Page" };
await navPage.PushAsync(page2);

await CreateHandlerAndAddToWindow<WindowHandlerStub>(new Window(navPage), async (handler) =>
{
await OnNavigatedToAsync(page2);
Assert.True(page2.HasNavigatedTo);
});

Assert.False(pageFiredNavigated);
}

#if !IOS && !MACCATALYST
[Fact(DisplayName = "Back Button Visibility Changes with push/pop")]
public async Task BackButtonVisibilityChangesWithPushPop()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,41 @@
using Xunit;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Controls.Handlers.Compatibility;
using Microsoft.Maui.DeviceTests.Stubs;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Hosting;
using UIKit;
using Xunit;

namespace Microsoft.Maui.DeviceTests
{
public partial class NavigationPageTests : ControlsHandlerTestBase
{
[Fact]
public async Task NavigatingBackViaBackButtonFiresNavigatedEvent()
{
SetupBuilder();
var page = new ContentPage();

var navPage = new NavigationPage(page) { Title = "App Page" };

await navPage.PushAsync(new ContentPage());
await CreateHandlerAndAddToWindow<WindowHandlerStub>(new Window(navPage), async (handler) =>
{
await OnNavigatedToAsync(navPage.CurrentPage);
var navController = navPage.Handler as UINavigationController;

Assert.False(page.HasNavigatedTo);
navController.NavigationBar.TapBackButton();
await OnNavigatedToAsync(page);
Assert.True(page.HasNavigatedTo);
});
}
}
}