Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,13 @@ protected async virtual void OnNavigateBack()
{
try
{
// Call OnBackButtonPressed to allow the page to intercept navigation
if (Page?.SendBackButtonPressed() == true)
// Route through Shell.OnBackButtonPressed so that Shell subclass overrides
// are invoked consistently for both the navigation bar back button and the
// hardware/system back button (fixes dotnet/maui#9095).
if (_shell?.SendBackButtonPressed() == true)
{
return;
}

await Page.Navigation.PopAsync();
}
Expand Down
162 changes: 162 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue9095.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 9095,
"Shell toolbar back button doesn't fire Shell.OnBackButtonPressed on Android and iOS",
PlatformAffected.Android)]
public class Issue9095 : TestShell
{
internal static bool BackButtonPressedCalledReturnFalse;
internal static bool ContentPageBackButtonPressedCalledReturnFalse;
Comment on lines +8 to +9

protected override void Init()
{
Routing.RegisterRoute(nameof(Issue9095SecondPage), typeof(Issue9095SecondPage));
Routing.RegisterRoute(nameof(Issue9095ReturnFalsePage), typeof(Issue9095ReturnFalsePage));
AddContentPage(new Issue9095RootPage());
}

protected override bool OnBackButtonPressed()
{
// Set static flag to prove Shell.OnBackButtonPressed was called
BackButtonPressedCalledReturnFalse = true;

if (CurrentPage is Issue9095SecondPage secondPage)
{
secondPage.UpdateStatus("OnBackButtonPressed Called");
}

// Delegate to base which calls ContentPage.OnBackButtonPressed internally.
// If ContentPage returns true → base returns true (navigation blocked).
// If ContentPage returns false → base pops the stack (navigation proceeds).
return base.OnBackButtonPressed();
}


public class Issue9095RootPage : ContentPage
{
readonly Label _returnFalseStatusLabel;
readonly Label _contentPageReturnFalseStatusLabel;

public Issue9095RootPage()
{
Title = "HomePage";
BackButtonPressedCalledReturnFalse = false;
ContentPageBackButtonPressedCalledReturnFalse = false;

var navigateButton = new Button
{
Text = "Go to Second Page",
AutomationId = "NavigateButton"
};

navigateButton.Clicked += async (s, e) =>
await Shell.Current.GoToAsync(nameof(Issue9095SecondPage));

var navigateReturnFalseButton = new Button
{
Text = "Go to Return False Page",
AutomationId = "NavigateReturnFalseButton"
};

navigateReturnFalseButton.Clicked += async (s, e) =>
await Shell.Current.GoToAsync(nameof(Issue9095ReturnFalsePage));

_returnFalseStatusLabel = new Label
{
Text = "Waiting",
AutomationId = "ReturnFalseStatusLabel"
};

_contentPageReturnFalseStatusLabel = new Label
{
Text = "Waiting",
AutomationId = "ContentPageReturnFalseStatusLabel"
};

Content = new VerticalStackLayout
{
Children = { navigateButton, navigateReturnFalseButton, _returnFalseStatusLabel, _contentPageReturnFalseStatusLabel }
};
}

protected override void OnAppearing()
{
base.OnAppearing();
if (BackButtonPressedCalledReturnFalse)
{
_returnFalseStatusLabel.Text = "OnBackButtonPressed Called And Returned False";
BackButtonPressedCalledReturnFalse = false;
}
if (ContentPageBackButtonPressedCalledReturnFalse)
{
_contentPageReturnFalseStatusLabel.Text = "ContentPage OnBackButtonPressed Called And Returned False";
ContentPageBackButtonPressedCalledReturnFalse = false;
Comment on lines +85 to +93
}
}
}

public class Issue9095SecondPage : ContentPage
{
readonly Label _statusLabel;
readonly Label _contentPageStatusLabel;

public Issue9095SecondPage()
{
Title = "Second Page";

_statusLabel = new Label
{
Text = "OnBackButtonPressed Not Called",
AutomationId = "BackButtonPressedLabel"
};

_contentPageStatusLabel = new Label
{
Text = "ContentPage OnBackButtonPressed Not Called",
AutomationId = "ContentPageBackButtonLabel"
};

Content = new VerticalStackLayout
{
Children = { _statusLabel, _contentPageStatusLabel }
};
}

public void UpdateStatus(string text)
{
_statusLabel.Text = text;
}

protected override bool OnBackButtonPressed()
{
_contentPageStatusLabel.Text = "ContentPage OnBackButtonPressed Called";
return true;
}
}

public class Issue9095ReturnFalsePage : ContentPage
{
public Issue9095ReturnFalsePage()
{
Title = "Return False Page";

Content = new VerticalStackLayout
{
Children =
{
new Label
{
Text = "Press back to test OnBackButtonPressed returning false",
AutomationId = "ReturnFalsePageLabel"
}
}
};
}

protected override bool OnBackButtonPressed()
{
ContentPageBackButtonPressedCalledReturnFalse = true;
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#if ANDROID // iOS/Mac fix pending in https://github.com/dotnet/maui/pull/35072
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue9095 : _IssuesUITest
{
public Issue9095(TestDevice device) : base(device) { }

public override string Issue => "Shell toolbar back button doesn't fire Shell.OnBackButtonPressed on Android and iOS";

protected override bool ResetAfterEachTest => true;

[Test]
[Category(UITestCategories.Shell)]
public void ShellOnBackButtonPressedShouldBeInvokedWhenPressingNavigationBarBackButton()
{
// Navigate to the second page
App.WaitForElement("NavigateButton");
App.Tap("NavigateButton");

// Wait for the second page to appear
App.WaitForElement("BackButtonPressedLabel");

// Verify the initial state
var initialText = App.FindElement("BackButtonPressedLabel").GetText();
Assert.That(initialText, Is.EqualTo("OnBackButtonPressed Not Called"),
"Label should show 'Not Called' before pressing back.");

var initialContentPageText = App.FindElement("ContentPageBackButtonLabel").GetText();
Assert.That(initialContentPageText, Is.EqualTo("ContentPage OnBackButtonPressed Not Called"),
"ContentPage label should show 'Not Called' before pressing back.");

// Tap the Shell toolbar back button
if (App is AppiumIOSApp iosApp && HelperExtensions.IsIOS26OrHigher(iosApp))
App.TapBackArrow(); // iOS 26+ doesn't show the previous page title in the back button
else
App.TapBackArrow(Device is TestDevice.iOS or TestDevice.Mac ? "HomePage" : "");

Comment on lines +36 to +41
// Shell.OnBackButtonPressed should have been called, updating the label.
// The second page remains visible because Shell.OnBackButtonPressed returns true.
var updatedText = App.WaitForElement("BackButtonPressedLabel").GetText();
Assert.That(updatedText, Is.EqualTo("OnBackButtonPressed Called"),
"Shell.OnBackButtonPressed should be invoked when pressing the Shell toolbar back button.");

// ContentPage.OnBackButtonPressed should also have been called.
var contentPageText = App.FindElement("ContentPageBackButtonLabel").GetText();
Assert.That(contentPageText, Is.EqualTo("ContentPage OnBackButtonPressed Called"),
"ContentPage.OnBackButtonPressed should be invoked when pressing the Shell toolbar back button.");
}

[Test]
[Category(UITestCategories.Shell)]
public void ShellOnBackButtonPressedReturnFalseShouldNavigateBack()
{
Comment on lines +54 to +57
// Navigate to the return-false page
App.WaitForElement("NavigateReturnFalseButton");
App.Tap("NavigateReturnFalseButton");

// Wait for the return-false page to appear
App.WaitForElement("ReturnFalsePageLabel");

// Tap the Shell toolbar back button
if (App is AppiumIOSApp iosApp && HelperExtensions.IsIOS26OrHigher(iosApp))
App.TapBackArrow();
else
App.TapBackArrow(Device is TestDevice.iOS or TestDevice.Mac ? "HomePage" : "");

// Shell.OnBackButtonPressed returned false, so navigation should proceed back to root.
// The labels on the root page confirm both Shell and ContentPage OnBackButtonPressed were called.
App.WaitForElement("ReturnFalseStatusLabel");
var shellStatusText = App.FindElement("ReturnFalseStatusLabel").GetText();
Assert.That(shellStatusText, Is.EqualTo("OnBackButtonPressed Called And Returned False"),
"Shell.OnBackButtonPressed should have been called even when returning false.");

var contentPageStatusText = App.FindElement("ContentPageReturnFalseStatusLabel").GetText();
Assert.That(contentPageStatusText, Is.EqualTo("ContentPage OnBackButtonPressed Called And Returned False"),
"ContentPage.OnBackButtonPressed should have been called even when returning false.");
}
}
#endif
Loading