[Windows] Fix Preserve ScrollView offsets when Orientation changes to Neither#34827
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34827Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34827" |
|
Hey there @@SubhikshaSf4851! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This PR addresses a Windows-specific ScrollView regression where switching ScrollOrientation to Neither resets the underlying WinUI ScrollViewer offsets back to (0,0). The fix updates the Windows ScrollViewer configuration to preserve offsets while still preventing user scrolling, and adds a HostApp repro page plus an Appium UI test.
Changes:
- Windows: update
ScrollViewerExtensions.UpdateScrollBarVisibilityto use hidden scrollbars (not disabled) and disable scroll modes when orientation isNeither. - Tests: add
Issue34671HostApp repro page and an Appium UI test validating offsets remain non-zero after switching toNeither.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Core/src/Platform/Windows/ScrollViewerExtensions.cs | Adjusts WinUI ScrollViewer scrollbar visibility/scroll modes for ScrollOrientation.Neither to preserve offsets. |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34671.cs | Adds an Appium UI test intended to verify scroll offsets are preserved when switching orientation to Neither. |
| src/Controls/tests/TestCases.HostApp/Issues/Issue34671.cs | Adds a manual repro page with buttons/labels to demonstrate and validate the orientation/offset behavior. |
| { | ||
| new Label | ||
| { | ||
| Text = "Repro for dotnet/maui#34671: scroll away from the origin, then set Orientation to Neither. On iOS, ScrollX and ScrollY reset to 0 instead of being preserved.", |
There was a problem hiding this comment.
The repro description text says "On iOS, ScrollX and ScrollY reset to 0" but this issue/PR is Windows-specific (#34671). This is misleading for manual validation (and for anyone using the page as a repro reference). Update the text to reference Windows (and/or all platforms if intended).
| Text = "Repro for dotnet/maui#34671: scroll away from the origin, then set Orientation to Neither. On iOS, ScrollX and ScrollY reset to 0 instead of being preserved.", | |
| Text = "Repro for dotnet/maui#34671: scroll away from the origin, then set Orientation to Neither. On Windows, ScrollX and ScrollY reset to 0 instead of being preserved.", |
| App.Tap("ScrollToReproOffsetButton"); | ||
| App.WaitForTextToBePresentInElement("LastActionLabel", "Scrolled to approx"); | ||
| App.WaitForElement("SetNeitherButton"); | ||
| App.Tap("SetNeitherButton"); | ||
|
|
||
| var offsetText = App.WaitForElement("OffsetLabel").GetText() | ||
| ?? throw new InvalidOperationException("OffsetLabel text was null."); | ||
| var offsetParts = offsetText.Split('|', StringSplitOptions.TrimEntries); |
There was a problem hiding this comment.
The test reads OffsetLabel immediately after tapping "SetNeitherButton" without waiting for the UI to update for the Neither action (the page updates labels asynchronously after a delay). This can make the test flaky and can also produce a false positive by asserting on the pre-change offsets. Add an explicit wait that confirms the Neither action has completed (e.g., wait for LastActionLabel/OrientationLabel to reflect Neither) before parsing OffsetLabel.
🚦 Gate — Test Before and After Fix
🚦 Gate Session —
|
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🖥️ Issue34671 Issue34671 |
✅ FAIL — 552s | ✅ PASS — 443s |
🔴 Without fix — 🖥️ Issue34671: FAIL ✅ · 552s
Determining projects to restore...
Restored D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj (in 44 sec).
Restored D:\a\1\s\src\Controls\Maps\src\Controls.Maps.csproj (in 43.92 sec).
Restored D:\a\1\s\src\Controls\Foldable\src\Controls.Foldable.csproj (in 369 ms).
Restored D:\a\1\s\src\BlazorWebView\src\Maui\Microsoft.AspNetCore.Components.WebView.Maui.csproj (in 3.67 sec).
Restored D:\a\1\s\src\Graphics\src\Graphics.Win2D\Graphics.Win2D.csproj (in 23 ms).
Restored D:\a\1\s\src\Essentials\src\Essentials.csproj (in 14 ms).
Restored D:\a\1\s\src\Core\src\Core.csproj (in 87 ms).
Restored D:\a\1\s\src\Core\maps\src\Maps.csproj (in 20 ms).
Restored D:\a\1\s\src\Graphics\src\Graphics\Graphics.csproj (in 3.59 sec).
Restored D:\a\1\s\src\Controls\src\Xaml\Controls.Xaml.csproj (in 24 ms).
Restored D:\a\1\s\src\Controls\tests\TestCases.HostApp\Controls.TestCases.HostApp.csproj (in 615 ms).
3 of 14 projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Graphics.Win2D -> D:\a\1\s\artifacts\bin\Graphics.Win2D\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Graphics.Win2D.WinUI.Desktop.dll
Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Core -> D:\a\1\s\artifacts\bin\Core\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.dll
Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Maps -> D:\a\1\s\artifacts\bin\Maps\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Maps.dll
Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Controls.Foldable -> D:\a\1\s\artifacts\bin\Controls.Foldable\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Foldable.dll
Microsoft.AspNetCore.Components.WebView.Maui -> D:\a\1\s\artifacts\bin\Microsoft.AspNetCore.Components.WebView.Maui\Debug\net10.0-windows10.0.19041.0\Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Xaml -> D:\a\1\s\artifacts\bin\Controls.Xaml\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Xaml.dll
Controls.Maps -> D:\a\1\s\artifacts\bin\Controls.Maps\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Maps.dll
Controls.TestCases.HostApp -> D:\a\1\s\artifacts\bin\Controls.TestCases.HostApp\Debug\net10.0-windows10.0.19041.0\win-x64\Controls.TestCases.HostApp.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:05:47.86
Determining projects to restore...
Restored D:\a\1\s\src\TestUtils\src\VisualTestUtils\VisualTestUtils.csproj (in 751 ms).
Restored D:\a\1\s\src\TestUtils\src\UITest.NUnit\UITest.NUnit.csproj (in 1.34 sec).
Restored D:\a\1\s\src\TestUtils\src\UITest.Core\UITest.Core.csproj (in 4 ms).
Restored D:\a\1\s\src\TestUtils\src\UITest.Appium\UITest.Appium.csproj (in 2.1 sec).
Restored D:\a\1\s\src\TestUtils\src\VisualTestUtils.MagickNet\VisualTestUtils.MagickNet.csproj (in 5.04 sec).
Restored D:\a\1\s\src\TestUtils\src\UITest.Analyzers\UITest.Analyzers.csproj (in 9.39 sec).
Restored D:\a\1\s\src\Controls\tests\CustomAttributes\Controls.CustomAttributes.csproj (in 5 ms).
Restored D:\a\1\s\src\Controls\tests\TestCases.WinUI.Tests\Controls.TestCases.WinUI.Tests.csproj (in 8.6 sec).
7 of 15 projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net10.0\Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net10.0\Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Controls.CustomAttributes -> D:\a\1\s\artifacts\bin\Controls.CustomAttributes\Debug\net10.0\Controls.CustomAttributes.dll
Core -> D:\a\1\s\artifacts\bin\Core\Debug\net10.0\Microsoft.Maui.dll
Controls.Core.Design -> D:\a\1\s\artifacts\bin\Controls.Core.Design\Debug\net472\Microsoft.Maui.Controls.DesignTools.dll
Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net10.0\Microsoft.Maui.Controls.dll
UITest.Core -> D:\a\1\s\artifacts\bin\UITest.Core\Debug\net10.0\UITest.Core.dll
UITest.Appium -> D:\a\1\s\artifacts\bin\UITest.Appium\Debug\net10.0\UITest.Appium.dll
UITest.NUnit -> D:\a\1\s\artifacts\bin\UITest.NUnit\Debug\net10.0\UITest.NUnit.dll
VisualTestUtils -> D:\a\1\s\artifacts\bin\VisualTestUtils\Debug\netstandard2.0\VisualTestUtils.dll
VisualTestUtils.MagickNet -> D:\a\1\s\artifacts\bin\VisualTestUtils.MagickNet\Debug\netstandard2.0\VisualTestUtils.MagickNet.dll
UITest.Analyzers -> D:\a\1\s\artifacts\bin\UITest.Analyzers\Debug\netstandard2.0\UITest.Analyzers.dll
Controls.TestCases.WinUI.Tests -> D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
Test run for D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.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.
D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 4/17/2026 9:49:26 AM FixtureSetup for Issue34671(Windows)
>>>>> 4/17/2026 9:49:36 AM Issue34671ScrollPositionIsPreservedWhenOrientationChangesToNeither Start
>>>>> 4/17/2026 9:49:39 AM Issue34671ScrollPositionIsPreservedWhenOrientationChangesToNeither Stop
>>>>> 4/17/2026 9:49:39 AM Log types:
Failed Issue34671ScrollPositionIsPreservedWhenOrientationChangesToNeither [4 s]
Error Message:
ScrollX should remain non-zero after setting orientation to Neither.
Assert.That(scrollX, Is.GreaterThan(0d))
Expected: greater than 0.0d
But was: 0.0d
Stack Trace:
at Microsoft.Maui.TestCases.Tests.Issues.Issue34671.Issue34671ScrollPositionIsPreservedWhenOrientationChangesToNeither() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34671.cs:line 37
1) at Microsoft.Maui.TestCases.Tests.Issues.Issue34671.Issue34671ScrollPositionIsPreservedWhenOrientationChangesToNeither() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34671.cs:line 37
NUnit Adapter 4.5.0.0: Test execution complete
[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.11] Discovering: Controls.TestCases.WinUI.Tests
[xUnit.net 00:00:00.30] Discovered: Controls.TestCases.WinUI.Tests
Total tests: 1
Failed: 1
Test Run Failed.
Total time: 33.1345 Seconds
🟢 With fix — 🖥️ Issue34671: PASS ✅ · 443s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Graphics.Win2D -> D:\a\1\s\artifacts\bin\Graphics.Win2D\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Graphics.Win2D.WinUI.Desktop.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Core -> D:\a\1\s\artifacts\bin\Core\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.dll
Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Maps -> D:\a\1\s\artifacts\bin\Maps\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Maps.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Controls.Maps -> D:\a\1\s\artifacts\bin\Controls.Maps\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Maps.dll
Microsoft.AspNetCore.Components.WebView.Maui -> D:\a\1\s\artifacts\bin\Microsoft.AspNetCore.Components.WebView.Maui\Debug\net10.0-windows10.0.19041.0\Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Xaml -> D:\a\1\s\artifacts\bin\Controls.Xaml\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Xaml.dll
Controls.Foldable -> D:\a\1\s\artifacts\bin\Controls.Foldable\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Foldable.dll
Controls.TestCases.HostApp -> D:\a\1\s\artifacts\bin\Controls.TestCases.HostApp\Debug\net10.0-windows10.0.19041.0\win-x64\Controls.TestCases.HostApp.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:05:30.34
Determining projects to restore...
All projects are up-to-date for restore.
Controls.CustomAttributes -> D:\a\1\s\artifacts\bin\Controls.CustomAttributes\Debug\net10.0\Controls.CustomAttributes.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net10.0\Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net10.0\Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Core -> D:\a\1\s\artifacts\bin\Core\Debug\net10.0\Microsoft.Maui.dll
Controls.Core.Design -> D:\a\1\s\artifacts\bin\Controls.Core.Design\Debug\net472\Microsoft.Maui.Controls.DesignTools.dll
Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13867296
Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net10.0\Microsoft.Maui.Controls.dll
UITest.Core -> D:\a\1\s\artifacts\bin\UITest.Core\Debug\net10.0\UITest.Core.dll
VisualTestUtils -> D:\a\1\s\artifacts\bin\VisualTestUtils\Debug\netstandard2.0\VisualTestUtils.dll
UITest.NUnit -> D:\a\1\s\artifacts\bin\UITest.NUnit\Debug\net10.0\UITest.NUnit.dll
VisualTestUtils.MagickNet -> D:\a\1\s\artifacts\bin\VisualTestUtils.MagickNet\Debug\netstandard2.0\VisualTestUtils.MagickNet.dll
UITest.Appium -> D:\a\1\s\artifacts\bin\UITest.Appium\Debug\net10.0\UITest.Appium.dll
UITest.Analyzers -> D:\a\1\s\artifacts\bin\UITest.Analyzers\Debug\netstandard2.0\UITest.Analyzers.dll
Controls.TestCases.WinUI.Tests -> D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
Test run for D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.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.
D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 4/17/2026 9:56:51 AM FixtureSetup for Issue34671(Windows)
>>>>> 4/17/2026 9:57:00 AM Issue34671ScrollPositionIsPreservedWhenOrientationChangesToNeither Start
>>>>> 4/17/2026 9:57:04 AM Issue34671ScrollPositionIsPreservedWhenOrientationChangesToNeither Stop
Passed Issue34671ScrollPositionIsPreservedWhenOrientationChangesToNeither [3 s]
NUnit Adapter 4.5.0.0: Test execution complete
[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.11] Discovering: Controls.TestCases.WinUI.Tests
[xUnit.net 00:00:00.39] Discovered: Controls.TestCases.WinUI.Tests
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 26.7323 Seconds
📁 Fix files reverted (1 files)
src/Core/src/Platform/Windows/ScrollViewerExtensions.cs
🤖 AI Summary
📊 Review Session —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34827 | Change ScrollBarVisibility.Disabled → Hidden + ScrollMode.Disabled when Neither; restore ScrollMode.Auto when leaving Neither |
✅ PASSED (Gate) | ScrollViewerExtensions.cs |
Windows-only platform fix |
🔬 Code Review — Deep Analysis
Code Review — PR #34827
Independent Assessment
What this changes: ScrollViewerExtensions.UpdateScrollBarVisibility previously applied ScrollBarVisibility.Disabled to both axes when orientation was set to Neither. This PR changes that to ScrollBarVisibility.Hidden + ScrollMode.Disabled. When leaving Neither, scroll modes are explicitly restored to Auto. Two new test files are added.
Inferred motivation: On WinUI, ScrollBarVisibility.Disabled carries a side-effect: the ScrollViewer resets its HorizontalOffset/VerticalOffset to (0,0). Switching to Hidden avoids that reset. ScrollMode.Disabled is then needed to prevent user interaction, since Hidden alone still allows scrolling.
Reconciliation with PR Narrative
Author claims: Root cause is confirmed — Disabled visibility causes WinUI to internally reset offsets. The fix uses Hidden + ScrollMode.Disabled to preserve position while blocking input. The PR also notes the || ANDROID test scope is temporary because PR #34672 (the iOS fix) has already merged to the inflight branch but not yet to main, and the gate requires a passing test.
Agreement: My independent assessment matches exactly. The author's root-cause analysis is accurate and the approach is correct for WinUI's documented ScrollBarVisibility semantics.
Findings
⚠️ Warning — CI is failing (maui-pr aggregate check)
maui-pr → fail and Build Analysis → fail. The individual build/pack/unit test sub-jobs all pass, and maui-pr-uitests is still pending. The aggregate failure should be resolved before merge — confirm whether this is a pre-existing infrastructure issue or caused by the UI tests completing with a failure.
⚠️ Warning — #if WINDOWS || ANDROID in test file, but fix is Windows-only
TestCases.Shared.Tests/Tests/Issues/Issue34671.cs:1 — The Issue attribute in the HostApp declares PlatformAffected.UWP, and the fix is entirely in ScrollViewerExtensions.cs (Windows). Including || ANDROID creates an implicit claim that Android behavior is being tested/fixed here. The PR description explains this as temporary (to pass the Gate while PR #34672 is pending), which is a reasonable workaround, but it should carry an inline comment and a tracked follow-up task to scope this back to #if WINDOWS once PR #34672 lands.
💡 Suggestion — double.Parse throws FormatException instead of a test-friendly assertion
TestCases.Shared.Tests/Tests/Issues/Issue34671.cs:34–35 — If the label text is ever malformed or truncated by Appium, double.Parse throws a raw FormatException rather than a NUnit assertion failure, making CI logs hard to diagnose. Prefer:
Assert.That(double.TryParse(scrollXText, NumberStyles.Any, CultureInfo.InvariantCulture, out var scrollX), Is.True,
$"Could not parse ScrollX from: '{scrollXText}'");💡 Suggestion — Redundant WaitForElement("SetNeitherButton") after label text wait
TestCases.Shared.Tests/Tests/Issues/Issue34671.cs:22–23 — The test already calls WaitForTextToBePresentInElement("LastActionLabel", "Scrolled to approx") which proves the scroll operation completed and the page is ready. The subsequent App.WaitForElement("SetNeitherButton") checks for a button that was visible at app launch and adds no synchronization value. It can be removed.
💡 Suggestion — Local Row/Column/Assign extension methods duplicate existing patterns
TestCases.HostApp/Issues/Issue34671.cs:265–287 — The Issue34671ViewExtensions class defines Row<TView>(), Column<TView>(), and Assign<TView>(). Other test pages in the same project use Grid.SetRow/Grid.SetColumn directly, and the project already has GridExtension.cs. These local fluent helpers are fine for a one-off page, but they pollute the Maui.Controls.Sample.Issues namespace globally and may shadow future shared helpers. Consider using Grid.SetRow/Grid.SetColumn inline, or extracting to the existing GridExtension.cs if the pattern is worth sharing.
Devil's Advocate
On the core fix: The approach (Hidden + ScrollMode.Disabled) is consistent with WinUI's documented semantics. The restore path (ScrollMode.Auto when leaving Neither) is correct because the subsequent ScrollBarVisibility.Disabled applied to inactive axes by the existing code still prevents scrolling in those directions via WinUI's own ScrollMode.Auto → follow-visibility logic.
On potential regression: MapOrientation passes only one ScrollBarVisibility value (the vertical one for Both). This is a pre-existing asymmetry, not introduced here. The new ScrollMode restoration code is symmetric and does not interact with that asymmetry.
On the || ANDROID test: Android's ScrollView already preserves scroll position on orientation changes (the bug is Windows-only), so the Android test passing is a free regression guard, not a false positive. It is documented as temporary. This is acceptable.
On the double.Parse concern: The label format is controlled entirely by the HostApp and is very unlikely to change mid-test. This is low-severity but genuinely makes failure investigation harder.
Verdict: NEEDS_DISCUSSION
Confidence: high
Summary: The production code fix (ScrollViewerExtensions.cs) is correct, minimal, well-commented, and touches no pre-existing regressions. The test code has minor quality issues (TryParse, redundant wait, wrong platform in description text), and the || ANDROID temporary test scope creates acknowledged short-term tech debt. The main blocker for merge is the failing maui-pr CI check — if that's a pre-existing infra issue unrelated to the PR's changes, the only actionable items are the suggestions above, and the fix is sound. The author should confirm CI status and the follow-up plan for narrowing the test to #if WINDOWS.
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix | Handler-level save/restore offsets in ScrollViewHandler.Windows.cs; extension method unchanged |
PASS | 1 file | More complex; fix in wrong abstraction layer |
| 2 | try-fix | ScrollMode.Disabled only (no visibility change) |
PASS | 1 file | Simpler but scrollbar chrome stays visible when Neither UX issue |
| 3 | try-fix | Reactive: capture offsets before Disabled, restore via ViewChanged+ChangeView | FAIL | 1 file | WinUI ignores ChangeView while in Disabled visibility state |
| 4 | try-fix | Hidden visibility only (no ScrollMode change) |
PASS | 1 file | Simpler but user can still scroll when Neither correctness issue |
| PR | PR #34827 | Hidden + ScrollMode.Disabled in extension method; restore ScrollMode.Auto on exit |
PASSED (Gate) | ScrollViewerExtensions.cs |
Complete: preserves offsets, hides chrome, blocks user scrolling |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | Yes | Opacity=0 on scrollbar template parts via visual-tree lookup more complex, fragile |
| claude-sonnet-4.6 | 2 | No | NO NEW IDEAS endorses PR fix as most complete |
| gpt-5.3-codex | 2 | Yes | Event interception (PointerWheelChanged, keyboard) overly complex and fragile |
| gpt-5.4 | 2 | Yes | Same event-interception idea clearly inferior |
Exhausted: Yes all approaches explored; new ideas from cross-pollination are strictly worse than PR fix
Selected Fix: PR's fix Hidden + ScrollMode.Disabled in extension method
Reason: The PR's fix is the only approach that satisfies all three requirements simultaneously: (1) preserves scroll offsets (Hidden doesn't trigger WinUI offset reset), (2) hides scrollbar chrome (matching expected UX for Neither orientation), and (3) prevents user scrolling (ScrollMode.Disabled). Attempt 2 passes the test but leaves scrollbars visible; Attempt 4 passes the test but allows user scrolling. The PR's compound approach is correct and minimal for this constraint set.
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issue #34671 / PR #34827 context gathered; prior Copilot inline review imported |
| Code Review | NEEDS_DISCUSSION (high) | 0 errors, 2 warnings, 3 suggestions |
| Gate | ✅ PASSED | Windows — tests FAIL without fix, PASS with fix |
| Try-Fix | ✅ COMPLETE | 4 attempts: 3 passing, 1 failing |
| Report | ✅ COMPLETE |
Code Review Impact on Try-Fix
The code review warning about ScrollMode restoration on exit from Neither directly guided exploration of edge cases in try-fix models. Attempt 4 (Hidden only, no ScrollMode) empirically confirmed the correctness concern: the test passed but user scrolling remained unblocked, validating that the PR's compound Hidden + ScrollMode.Disabled approach is required for correct behavior. Attempt 2 (ScrollMode.Disabled only) confirmed the scrollbar chrome UX concern. Together, these attempts validated every component of the PR's fix is necessary.
Summary
The production fix in ScrollViewerExtensions.cs is correct, minimal, and well-targeted — switching from ScrollBarVisibility.Disabled to Hidden + ScrollMode.Disabled is the right compound solution. Four independent fix explorations confirm no simpler correct alternative exists. The blocking concerns are in the test code: the test lacks a synchronization wait after tapping "SetNeitherButton", creating a potential race with the async 100ms Task.Delay in the button handler; the HostApp description text says "On iOS" (wrong platform); and the #if WINDOWS || ANDROID condition needs an inline comment explaining it's temporary.
Root Cause
On WinUI, ScrollBarVisibility.Disabled has a side-effect: the ScrollViewer resets its HorizontalOffset/VerticalOffset to (0,0). This happens internally in WinUI's layout pass when Disabled visibility is applied. Switching to Hidden preserves the content offset. ScrollMode.Disabled is then required to prevent user input from scrolling while orientation is Neither.
Fix Quality
The production fix (ScrollViewerExtensions.cs) is high quality:
- ✅ Correct root-cause analysis (confirmed by Try-Fix Attempt 3 which proved reactive restoration doesn't work — the fix must be preventive)
- ✅ Minimal change (6 lines added, 1 line changed)
- ✅ Well-commented explaining the WHY
- ✅ Handles both entry to and exit from
Neither(ScrollMode restoration) - ✅ No blast radius on other orientations
Test code has three issues that should be addressed before merge:
- Flakiness risk (
⚠️ prior review + code review):TestCases.Shared.Tests/Tests/Issues/Issue34671.cs:27— the test readsOffsetLabelimmediately after tappingSetNeitherButton, but the HostApp'sOnSetNeitherClickedupdates labels only afterawait Task.Delay(100). There's no wait forLastActionLabelto confirm the "Set Orientation to Neither" update completed. This is a potential false-positive or flaky failure. Fix: addApp.WaitForTextToBePresentInElement("LastActionLabel", "Set Orientation to Neither")before readingOffsetLabel. - Wrong platform text (💡 prior review):
TestCases.HostApp/Issues/Issue34671.cs:61— description says "On iOS" but this is a Windows bug. - Missing inline comment on
#if WINDOWS || ANDROID(⚠️ code review): The PR description explains this is temporary, but the code should have an inline comment so future maintainers understand why Android is included and when to narrow it back to#if WINDOWS.
…o Neither (#34827) <!-- Please keep the note below for people who find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment whether this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Root Cause On Windows, when `ScrollOrientation` was set to `Neither`, the `ScrollViewer` used `Disabled` scroll modes and scroll bars. Disabling scrolling caused the control to internally reset its scroll offsets to the origin `(0,0)`, instead of preserving the current position. ### Description of Change **Bug fix for ScrollView offset preservation:** * Updated `ScrollViewerExtensions.cs` so that when `ScrollOrientation.Neither` is set, the scroll bars are set to `Hidden` (instead of `Disabled`) and the scroll modes are set to `Disabled`. This preserves the current scroll position instead of resetting it to (0,0). When leaving `Neither`, scroll modes are restored to `Auto`. ### Testing : * Added a new manual test page `Issue34671.cs` that demonstrates and allows manual verification of the bug and its fix. The page provides controls to change orientation, scroll to a specific offset, and reset the state, as well as labels to display current scroll position and actions. * Added a new automated UI test `Issue34671.cs` (for Windows and Android) that verifies the scroll position remains non-zero after changing orientation to `Neither`, ensuring the fix is validated in CI. ### Note : The Test for this fix is already covered in [PR #34672](#34672), which has been merged into the inflight branch. The same test has been included here temporarily so the Gate phase passes. Once PR #34672 is merged into `main`, the duplicate test in this PR can be reverted, and the Windows-specific test from PR #34672 can be enabled. <!-- Enter description of the fix in this section --> ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #34671 ### Tested the behavior in the following platforms - [x] Windows - [ ] Android - [ ] iOS - [ ] Mac | Before Issue Fix | After Issue Fix | |----------|----------| | <video src="https://github.com/user-attachments/assets/353f4dbc-a009-4035-a907-e52f6a5ca7ff"> | <video src="https://github.com/user-attachments/assets/ea0891cd-7b7c-433d-9285-64118bf0e0eb"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. -->
…o Neither (#34827) <!-- Please keep the note below for people who find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment whether this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Root Cause On Windows, when `ScrollOrientation` was set to `Neither`, the `ScrollViewer` used `Disabled` scroll modes and scroll bars. Disabling scrolling caused the control to internally reset its scroll offsets to the origin `(0,0)`, instead of preserving the current position. ### Description of Change **Bug fix for ScrollView offset preservation:** * Updated `ScrollViewerExtensions.cs` so that when `ScrollOrientation.Neither` is set, the scroll bars are set to `Hidden` (instead of `Disabled`) and the scroll modes are set to `Disabled`. This preserves the current scroll position instead of resetting it to (0,0). When leaving `Neither`, scroll modes are restored to `Auto`. ### Testing : * Added a new manual test page `Issue34671.cs` that demonstrates and allows manual verification of the bug and its fix. The page provides controls to change orientation, scroll to a specific offset, and reset the state, as well as labels to display current scroll position and actions. * Added a new automated UI test `Issue34671.cs` (for Windows and Android) that verifies the scroll position remains non-zero after changing orientation to `Neither`, ensuring the fix is validated in CI. ### Note : The Test for this fix is already covered in [PR #34672](#34672), which has been merged into the inflight branch. The same test has been included here temporarily so the Gate phase passes. Once PR #34672 is merged into `main`, the duplicate test in this PR can be reverted, and the Windows-specific test from PR #34672 can be enabled. <!-- Enter description of the fix in this section --> ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #34671 ### Tested the behavior in the following platforms - [x] Windows - [ ] Android - [ ] iOS - [ ] Mac | Before Issue Fix | After Issue Fix | |----------|----------| | <video src="https://github.com/user-attachments/assets/353f4dbc-a009-4035-a907-e52f6a5ca7ff"> | <video src="https://github.com/user-attachments/assets/ea0891cd-7b7c-433d-9285-64118bf0e0eb"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. -->
…o Neither (#34827) <!-- Please keep the note below for people who find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment whether this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Root Cause On Windows, when `ScrollOrientation` was set to `Neither`, the `ScrollViewer` used `Disabled` scroll modes and scroll bars. Disabling scrolling caused the control to internally reset its scroll offsets to the origin `(0,0)`, instead of preserving the current position. ### Description of Change **Bug fix for ScrollView offset preservation:** * Updated `ScrollViewerExtensions.cs` so that when `ScrollOrientation.Neither` is set, the scroll bars are set to `Hidden` (instead of `Disabled`) and the scroll modes are set to `Disabled`. This preserves the current scroll position instead of resetting it to (0,0). When leaving `Neither`, scroll modes are restored to `Auto`. ### Testing : * Added a new manual test page `Issue34671.cs` that demonstrates and allows manual verification of the bug and its fix. The page provides controls to change orientation, scroll to a specific offset, and reset the state, as well as labels to display current scroll position and actions. * Added a new automated UI test `Issue34671.cs` (for Windows and Android) that verifies the scroll position remains non-zero after changing orientation to `Neither`, ensuring the fix is validated in CI. ### Note : The Test for this fix is already covered in [PR #34672](#34672), which has been merged into the inflight branch. The same test has been included here temporarily so the Gate phase passes. Once PR #34672 is merged into `main`, the duplicate test in this PR can be reverted, and the Windows-specific test from PR #34672 can be enabled. <!-- Enter description of the fix in this section --> ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #34671 ### Tested the behavior in the following platforms - [x] Windows - [ ] Android - [ ] iOS - [ ] Mac | Before Issue Fix | After Issue Fix | |----------|----------| | <video src="https://github.com/user-attachments/assets/353f4dbc-a009-4035-a907-e52f6a5ca7ff"> | <video src="https://github.com/user-attachments/assets/ea0891cd-7b7c-433d-9285-64118bf0e0eb"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. -->
…o Neither (#34827) <!-- Please keep the note below for people who find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment whether this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Root Cause On Windows, when `ScrollOrientation` was set to `Neither`, the `ScrollViewer` used `Disabled` scroll modes and scroll bars. Disabling scrolling caused the control to internally reset its scroll offsets to the origin `(0,0)`, instead of preserving the current position. ### Description of Change **Bug fix for ScrollView offset preservation:** * Updated `ScrollViewerExtensions.cs` so that when `ScrollOrientation.Neither` is set, the scroll bars are set to `Hidden` (instead of `Disabled`) and the scroll modes are set to `Disabled`. This preserves the current scroll position instead of resetting it to (0,0). When leaving `Neither`, scroll modes are restored to `Auto`. ### Testing : * Added a new manual test page `Issue34671.cs` that demonstrates and allows manual verification of the bug and its fix. The page provides controls to change orientation, scroll to a specific offset, and reset the state, as well as labels to display current scroll position and actions. * Added a new automated UI test `Issue34671.cs` (for Windows and Android) that verifies the scroll position remains non-zero after changing orientation to `Neither`, ensuring the fix is validated in CI. ### Note : The Test for this fix is already covered in [PR #34672](#34672), which has been merged into the inflight branch. The same test has been included here temporarily so the Gate phase passes. Once PR #34672 is merged into `main`, the duplicate test in this PR can be reverted, and the Windows-specific test from PR #34672 can be enabled. <!-- Enter description of the fix in this section --> ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #34671 ### Tested the behavior in the following platforms - [x] Windows - [ ] Android - [ ] iOS - [ ] Mac | Before Issue Fix | After Issue Fix | |----------|----------| | <video src="https://github.com/user-attachments/assets/353f4dbc-a009-4035-a907-e52f6a5ca7ff"> | <video src="https://github.com/user-attachments/assets/ea0891cd-7b7c-433d-9285-64118bf0e0eb"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. -->
…hanges to Neither (dotnet#34827)" This reverts commit e2f16fc.
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment whether this change resolves your issue. Thank you!
Root Cause
On Windows, when
ScrollOrientationwas set toNeither, theScrollViewerusedDisabledscroll modes and scroll bars. Disabling scrolling caused the control to internally reset its scroll offsets to the origin(0,0), instead of preserving the current position.Description of Change
Bug fix for ScrollView offset preservation:
ScrollViewerExtensions.csso that whenScrollOrientation.Neitheris set, the scroll bars are set toHidden(instead ofDisabled) and the scroll modes are set toDisabled. This preserves the current scroll position instead of resetting it to (0,0). When leavingNeither, scroll modes are restored toAuto.Testing :
Issue34671.csthat demonstrates and allows manual verification of the bug and its fix. The page provides controls to change orientation, scroll to a specific offset, and reset the state, as well as labels to display current scroll position and actions.Issue34671.cs(for Windows and Android) that verifies the scroll position remains non-zero after changing orientation toNeither, ensuring the fix is validated in CI.Note :
The Test for this fix is already covered in PR #34672, which has been merged into the inflight branch. The same test has been included here temporarily so the Gate phase passes. Once PR #34672 is merged into
main, the duplicate test in this PR can be reverted, and the Windows-specific test from PR #34672 can be enabled.Issues Fixed
Fixes #34671
Tested the behavior in the following platforms
BeforeFix34671.mp4
AfterFix34671.1.mp4