[Android] PopToRootAsync for modal pages - improvements#26851
Conversation
|
Hi @pictos what do you think about this? |
ab33933 to
09c6260
Compare
src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs
Outdated
Show resolved
Hide resolved
|
We are affected by this as well any estimate when this will be merged ? thanks |
|
Hi, this pull request looks promising! Is there any update on when this will get reviewed? |
|
Hi @Zack-G-I-T probably next week :) |
|
@kubaflo hi is there any chance of this making next release? thanks a lot! |
|
Hi @gabsamples6 it is not up to me. However, I think that there's a chance. cheers! |
PureWeen
left a comment
There was a problem hiding this comment.
This implementation causes pages to just be popped FIFO instead of LIFO
I created a basic sample that just pops modal pages and you'll notice that as you pop modal pages only the first one gets popped not the currently visible on.
Can you add a unit test that would have caught this?
public MainPage()
{
InitializeComponent();
asdf.Clicked += async (s, e) =>
{
var button = (s as IView);
await Navigation.PushModalAsync(new NavigationPage(new MainPage(){ Title = $"Modal Page: {_count++}" }));
};
asdf2.Clicked += async (s, e) =>
{
await Navigation.PopModalAsync(false);
};
}
Hmm, I'm not sure I understand your case I've created this sample Content = new VerticalStackLayout
{
Children = {
new Label { Text = "This is a modal page1" },
new Button { Text = "Open modal page2", Command = new Command(async () => await Navigation.PushModalAsync(new ContentPage()
{
Content = new VerticalStackLayout
{
Children = {
new Label { Text = "This is a modal page2" },
new Button { Text = "Open modal page3", Command = new Command(async () => await Navigation.PushModalAsync(new ContentPage()
{
Content = new VerticalStackLayout
{
Children = {
new Label { Text = "This is a modal page3" },
new Button { Text = "Close", Command = new Command(async () => await Navigation.PopModalAsync()) }
}
}
})) },
new Button { Text = "Close", Command = new Command(async () => await Navigation.PopModalAsync()) }
}
}
})) },
new Button { Text = "Close", Command = new Command(async () => await Navigation.PopModalAsync()) }
}
}And it appears to be working fine Screen.Recording.2025-02-01.at.16.55.12.mov |
09c6260 to
b6bb662
Compare
|
@kubaflo I'm sorry, I did a really poor job giving you all the details here. I've pushed up a commit with your sample modified slightly to demonstrate what I was talking about |
b6bb662 to
c5d6e7d
Compare
src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.cs
Outdated
Show resolved
Hide resolved
Can you add a UI test that would have caught this? |
c5d6e7d to
10334fa
Compare
|
@PureWeen I've added a test :) |
This comment was marked as off-topic.
This comment was marked as off-topic.
10334fa to
6cd1144
Compare
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 26851Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 26851" |
There was a problem hiding this comment.
Pull request overview
Addresses Shell modal-stack flicker during PopToRootAsync(false) by changing the modal pop order in platform modal navigation managers and adding a repro page + UI test scaffolding.
Changes:
- Add
ShellSection.IsPoppingModalStackToRootflag and set it duringOnPopToRootAsyncto inform modal popping behavior. - When popping to root without animation, remove/dismiss modal pages from the bottom of the modal stack to reduce intermediate-page flashing (Android + iOS + shared manager logic).
- Add HostApp repro (
Issue26846) and a corresponding UI test file.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue26846.cs | Adds a UI test for Issue 26846 scenario (currently doesn’t exercise PopToRoot). |
| src/Controls/tests/TestCases.HostApp/Issues/Issue26846.cs | Adds a Shell-based repro page with modal push/pop and PopToRoot actions. |
| src/Controls/src/Core/Shell/ShellSection.cs | Introduces and toggles a new flag to indicate “popping modal stack to root”. |
| src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.cs | Adjusts modal popping selection to pop from bottom during PopToRoot without animation. |
| src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs | Switches modal tag tracking to an ordered list and supports bottom-pop dismissal. |
| src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.iOS.cs | Supports bottom-pop dismissal when popping to root without animation. |
| protected override async Task OnPopToRootAsync(bool animated) | ||
| { | ||
| _owner.IsPoppingModalStackToRoot = true; | ||
|
|
||
| if (!_owner.IsVisibleSection) | ||
| { | ||
| return _owner.OnPopToRootAsync(animated); | ||
| await _owner.OnPopToRootAsync(animated); | ||
| _owner.IsPoppingModalStackToRoot = false; | ||
| return; |
|
|
||
| // If we are popping multiple pages and animation is disabled, | ||
| // remove pages from the bottom of the stack to avoid visual flickering. | ||
| if (_isPoppingModalStackToRoot && !animated) |
| [Test] | ||
| [Category(UITestCategories.Navigation)] | ||
| public void ModalPoppingShouldWorkOneByOne() | ||
| { | ||
| App.WaitForElement("OpenModalPage2"); |
- Wrap IsPoppingModalStackToRoot in try/finally to ensure flag reset on exceptions - Use DismissNow() instead of Dismiss() for non-animated Android modal dismissal to commit fragment removal synchronously and avoid intermediate frame rendering - Add PopToRootShouldDismissAllModalsInstantly test that exercises the actual PopToRootAsync path (the existing test only closed modals one-by-one) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🚦 Gate - Test Before and After Fix📊 Expand Full Gate —
|
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🖥️ Issue26846 Issue26846 |
✅ FAIL — 1269s | ✅ PASS — 623s |
🔴 Without fix — 🖥️ Issue26846: FAIL ✅ · 1269s
(truncated to last 15,000 chars)
r executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;94m[bdd18454]�[0m�[38;5;-176m[AndroidUiautomator2Driver@262a]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;212m[275df952]�[0m�[38;5;-91m[AndroidUiautomator2Driver@7131]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;142m[9c9e00b8]�[0m�[38;5;-167m[AndroidUiautomator2Driver@4299]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;227m[3a02409d]�[0m�[38;5;-23m[AndroidUiautomator2Driver@40df]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;32m[7632f51d]�[0m�[38;5;-75m[AndroidUiautomator2Driver@ffdb]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
>>>>> 03/31/2026 11:52:39 The SaveDeviceDiagnosticInfo threw an exception during Issue26846(Android).
Exception details: System.InvalidOperationException: Call InitialSetup before accessing the App property.
at UITest.Appium.NUnit.UITestContextBase.get_App() in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 32
at UITest.Appium.NUnit.UITestBase.SaveDeviceDiagnosticInfo(String note, Boolean storeForReattachment) in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 255
TearDown failed for test fixture Microsoft.Maui.TestCases.Tests.Issues.Issue26846(Android)
OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings delete global hidden_api_policy_pre_p_apps;settings delete global hidden_api_policy_p_apps;settings delete global hidden_api_policy'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
TearDown : System.InvalidOperationException : Call InitialSetup before accessing the App property.
StackTrace: at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions)
at OpenQA.Selenium.Appium.Android.AndroidDriver..ctor(Uri remoteAddress, DriverOptions driverOptions)
at UITest.Appium.AppiumAndroidApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 11
at UITest.Appium.AppiumAndroidApp.CreateAndroidApp(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 41
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 42
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
--TearDown
at UITest.Appium.NUnit.UITestContextBase.get_App() in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 32
at UITest.Appium.NUnit.UITestBase.OneTimeTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 244
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
NUnit Adapter 4.5.0.0: Test execution complete
Failed ModalPoppingShouldWorkOneByOne [7 m 12 s]
Error Message:
OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings delete global hidden_api_policy_pre_p_apps;settings delete global hidden_api_policy_p_apps;settings delete global hidden_api_policy'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
Stack Trace:
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions)
at OpenQA.Selenium.Appium.Android.AndroidDriver..ctor(Uri remoteAddress, DriverOptions driverOptions)
at UITest.Appium.AppiumAndroidApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 11
at UITest.Appium.AppiumAndroidApp.CreateAndroidApp(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 41
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 42
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Failed PopToRootShouldDismissAllModalsInstantly [7 m 12 s]
Error Message:
OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings delete global hidden_api_policy_pre_p_apps;settings delete global hidden_api_policy_p_apps;settings delete global hidden_api_policy'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
Stack Trace:
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions)
at OpenQA.Selenium.Appium.Android.AndroidDriver..ctor(Uri remoteAddress, DriverOptions driverOptions)
at UITest.Appium.AppiumAndroidApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 11
at UITest.Appium.AppiumAndroidApp.CreateAndroidApp(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 41
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 42
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Total tests: 2
Test Run Failed.
Failed: 2
Total time: 7.3642 Minutes
🟢 With fix — 🖥️ Issue26846: PASS ✅ · 623s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0-android36.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0-android36.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0-android36.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Maps -> /home/vsts/work/1/s/artifacts/bin/Maps/Debug/net10.0-android36.0/Microsoft.Maui.Maps.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-android36.0/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Foldable.dll
Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-android36.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Xaml.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Maps.dll
Controls.TestCases.HostApp -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Controls.TestCases.HostApp.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.dll
Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Foldable.dll
Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Xaml.dll
Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Maps.dll
Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.AspNetCore.Components.WebView.Maui.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:08:09.33
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
Controls.CustomAttributes -> /home/vsts/work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13698802
Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
UITest.Core -> /home/vsts/work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
UITest.Appium -> /home/vsts/work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
VisualTestUtils -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
UITest.NUnit -> /home/vsts/work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
VisualTestUtils.MagickNet -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
UITest.Analyzers -> /home/vsts/work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
Controls.TestCases.Android.Tests -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.16] Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.39] Discovered: Controls.TestCases.Android.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
NUnit3TestExecutor discovered 2 of 2 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 03/31/2026 12:02:40 FixtureSetup for Issue26846(Android)
>>>>> 03/31/2026 12:02:43 ModalPoppingShouldWorkOneByOne Start
>>>>> 03/31/2026 12:02:52 ModalPoppingShouldWorkOneByOne Stop
Passed ModalPoppingShouldWorkOneByOne [9 s]
>>>>> 03/31/2026 12:02:52 PopToRootShouldDismissAllModalsInstantly Start
>>>>> 03/31/2026 12:02:57 PopToRootShouldDismissAllModalsInstantly Stop
Passed PopToRootShouldDismissAllModalsInstantly [5 s]
NUnit Adapter 4.5.0.0: Test execution complete
Test Run Successful.
Total tests: 2
Passed: 2
Total time: 36.7812 Seconds
📁 Fix files reverted (2 files)
eng/pipelines/ci-copilot.ymlsrc/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs
🤖 AI Summary📊 Expand Full Review —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #26851 | Check IsPoppingModalStack on ShellSection; use DismissNow() for batch pops, Dismiss() otherwise |
✅ PASSED (Gate) | ModalNavigationManager.Android.cs (+11/-1 lines) |
Android-only, Shell-scoped, minimal |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | Dismiss() + fragmentManager.ExecutePendingTransactions() to force synchronous commit |
✅ PASS | ModalNavigationManager.Android.cs (+6) |
No Shell coupling; universal; slightly risky (flushes all pending transactions) |
| 2 | try-fix (claude-sonnet-4.6) | Hide fragment view (ViewStates.Invisible) before Dismiss() to prevent visual flash |
✅ PASS | ModalNavigationManager.Android.cs (+7) |
No Shell coupling; visual-layer; workaround feel |
| 3 | try-fix (gpt-5.3-codex) | DismissNow() when _modals.Count > 0 (more to pop), Dismiss() for last modal |
✅ PASS | ModalNavigationManager.Android.cs (+7) |
No Shell coupling; self-contained logic; elegant |
| 4 | try-fix (gpt-5.4, gemini unavailable) | Set dialog.Window.DecorView.Alpha = 0f before Dismiss() to instantly hide window |
✅ PASS | ModalNavigationManager.Android.cs (+2) |
No Shell coupling; window-layer; cosmetic workaround |
| 5 | try-fix (claude-opus-4.6, cross-poll) | fragmentManager.BeginTransaction().Remove(dialogFragment).CommitNow() replacing Dismiss() |
✅ PASS | ModalNavigationManager.Android.cs (+8/-1) |
No Shell coupling; semantically correct; cleanest API fix |
| PR | PR #26851 | Check ShellSection.IsPoppingModalStack; use DismissNow() for batch pops, Dismiss() otherwise |
✅ PASSED (Gate) | ModalNavigationManager.Android.cs (+11/-1) |
Shell-scoped; doesn't cover NavigationPage modal scenarios |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | Yes | Batch FragmentTransaction approach → implemented as Attempt 5 |
| claude-sonnet-4.6 | 2 | Yes | Same batch-transaction idea → covered by Attempt 5 |
| gpt-5.3-codex | 2 | Yes | Multiple ideas (suppress layout, Dialog.Hide, animation disable) — all variations of prior approaches |
| gpt-5.4 | 2 | Yes | Batch pop API (large architectural change, out of scope); snapshot overlay (complex); Dialog.Hide (variation of Attempt 2) |
| All models | 3 | No new unique implementable ideas | Exhausted |
Exhausted: Yes
Selected Fix: Attempt 3 (stack-depth DismissNow) or Attempt 5 (BeginTransaction.Remove.CommitNow) over PR's fix
Comparison:
| Criterion | PR Fix | Attempt 3 (_modals.Count check) |
Attempt 5 (BeginTransaction.Remove.CommitNow) |
|---|---|---|---|
| Lines changed | +11/-1 | +7 | +8/-1 |
| Shell coupling | ✅ Yes (requires Shell+ShellSection traversal) | ❌ None | ❌ None |
| Covers NavigationPage modals | ❌ No | ✅ Yes | ✅ Yes |
| Root cause addressed | Partial (deferred for non-Shell) | ✅ Yes | ✅ Yes (most direct) |
| Semantic clarity | Clear | Clear | Clearest (replaces wrong API) |
| Side-effect risk | Low (scoped) | Low | Low |
| Test validation | ✅ PASS (Gate) | ✅ PASS | ✅ PASS |
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issue #26846; Android modal flash on PopToRootAsync; 1 impl file, 2 test files |
| Gate | ✅ PASSED | Android — test FAILS without fix (timeout), PASSES with fix |
| Try-Fix | ✅ COMPLETE | 5 attempts + 2 cross-pollination rounds; all passing; better alternatives found |
| Report | ✅ COMPLETE |
Summary
PR #26851 fixes a real .NET 9 regression where PopToRootAsync(false) with multiple modal pages causes intermediate modals to briefly flash on screen before dismissal. The fix is technically sound and the Gate now passes. However, better alternatives exist that solve the same problem without coupling to Shell internals — specifically Attempt 3 and Attempt 5 from the try-fix phase, which are simpler, more universal, and cover NavigationPage-based modal scenarios that the PR's fix ignores.
Root Cause
dialogFragment.Dismiss() internally calls FragmentTransaction.commit(), scheduling fragment removal for the next layout pass. When PopToRootAsync loops through multiple modals in sequence, each commit() defers to a separate frame — the previously-covered modal briefly becomes the top-most visible content during that frame before being removed. This manifests as a flash.
Fix Quality
PR's fix — IsPoppingModalStack + DismissNow():
- ✅ Gate passes; test correctly catches the regression
- ✅ Scoped: only uses
DismissNow()for batch pops, preservingDismiss()elsewhere - ❌ Shell-specific coupling: accesses
shell.CurrentItem?.CurrentItem?.IsPoppingModalStack— tight coupling from platform layer (ModalNavigationManager) up toShellSectioninternals - ❌ NavigationPage gap: if modals are pushed via
NavigationPage(not Shell),_window.Page is Shellis false and the fix doesn't activate — those users still see the flash
Best alternative — Attempt 3 (DismissNow() based on _modals.Count):
if (_modals.Count > 0)
dialogFragment.DismissNow();
else
dialogFragment.Dismiss();- ✅ No Shell coupling — logic lives entirely within
ModalNavigationManager - ✅ Covers all modal scenarios (Shell, NavigationPage, any page type)
- ✅ Self-explanatory: "if more modals remain after this pop, dismiss synchronously"
- ✅ 7 lines, no external state required
Strongest alternative — Attempt 5 (BeginTransaction().Remove().CommitNow()):
fragmentManager.BeginTransaction()
.Remove(dialogFragment)
.CommitNow();- ✅ Most semantically correct: replaces the fundamentally wrong
Dismiss()(which uses asynccommit()) with the proper synchronous API - ✅ Universal — no coupling, no state inspection
- ✅
OnDismiss()lifecycle callback still fires (DialogFragment triggers it fromonDestroyView()when dialog is showing) - ✅ +8/-1 lines
Required Changes
Primary: Replace the Shell-specific detection (_window.Page is Shell shell && shell.CurrentItem?.CurrentItem?.IsPoppingModalStack) with a self-contained approach. Two viable options:
Option A — Recommended (Attempt 3, stack-depth check):
if (_modals.Count > 0)
dialogFragment.DismissNow();
else
dialogFragment.Dismiss();
source.TrySetResult(modal);Option B — Semantically cleanest (Attempt 5, proper API):
fragmentManager.BeginTransaction()
.Remove(dialogFragment)
.CommitNow();
source.TrySetResult(modal);Both approaches:
- Pass the existing tests (confirmed empirically)
- Require fewer lines than the PR's fix
- Eliminate the Shell coupling concern entirely
- Protect all modal navigation patterns (not just Shell)
Notes
gemini-3-pro-previewwas unavailable;gpt-5.4used as fallback for attempt 4- The PR has already gone through one prior agent review cycle (labels:
s/agent-fix-win,s/agent-fix-implemented) - The Gate improvement from the prior review is confirmed — tests now correctly catch the regression
- The fix itself is a reasonable approach; the change requested is architectural simplification, not a correctness issue
Revert changes to ModalNavigationManager.cs (shared), ModalNavigationManager.iOS.cs, and ShellSection.cs. Instead of changing pop order (FIFO) and using DismissNow() for ALL non-animated pops, scope DismissNow() to only batch-dismiss scenarios by checking the existing IsPoppingModalStack flag on ShellSection. This fixes the gate failure where ModalPoppingShouldWorkOneByOne broke because DismissNow() was applied too broadly to single modal pops. The scoped approach: - Uses existing IsPoppingModalStack flag (no new IsPoppingModalStackToRoot needed) - Only applies DismissNow() during PopToRoot batch pops - Keeps correct LIFO pop order (no Stack->List conversion needed) - Changes only 1 implementation file (~10 lines) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Applied the recommended fix from the AI review (Attempt 1 / claude-opus-4.6): Changes in
Why this is better than the previous approach:
|
Code Review — PR #26851Independent AssessmentWhat this changes: In Android's How it detects batch popping: Reuses the existing
Inferred motivation: Reconciliation with PR NarrativeAuthor claims: Fixes #26846 — Shell Previous reviewer feedback:
Findings
|
🟢 .NET MAUI Review - ApprovedExpand Full Review -
|
🔍 Prior Fix Regression CheckResult: ✅ No prior fix regressions detected Checked deleted lines across implementation files. No lines were identified as reversions of prior bug fixes.
|
Issues Fixed
Fixes #26846
Screen.Recording.2024-12-27.at.20.46.44.mov
Screen.Recording.2024-12-27.at.20.45.08.mov