Skip to content

[Android] - Fix KeepScrollOffset Behavior During Dynamic Item Additions in CollectionView - #29255

Merged
kubaflo merged 4 commits into
dotnet:inflight/currentfrom
prakashKannanSf3972:fix-29131
Jun 7, 2026
Merged

[Android] - Fix KeepScrollOffset Behavior During Dynamic Item Additions in CollectionView#29255
kubaflo merged 4 commits into
dotnet:inflight/currentfrom
prakashKannanSf3972:fix-29131

Conversation

@prakashKannanSf3972

@prakashKannanSf3972 prakashKannanSf3972 commented Apr 29, 2025

Copy link
Copy Markdown
Contributor

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 if this change resolves your issue. Thank you!

Issue Details

KeepScrollOffset mode does not maintain scroll position when new items are inserted into the CollectionView, resulting in unintended upward scrolling.

Root Cause

When new items are added to the collection, the scroll offset is recalculated based on the updated layout. This results in incorrect offset adjustments, causing the scroll position to shift upward instead of being correctly preserved.

Description of Change

  • Added AddScrollListener and RemoveScrollListener methods to manage scroll listeners dynamically. Adjusted the TrackOffsets method to ensure scroll offsets are only adjusted when the first item is reached, ensuring accurate scroll behavior and preventing unintended upward scrolling during dynamic item insertions.

Issues Fixed

Fixes #29131

  • The Following PR - 27153 fixes the KeepItemsInView issue on Android.

Tested the behaviour in the following platforms

  • Android
  • Windows
  • iOS
  • Mac

Output

Before After
BeforeFix_ScrollOffset.mov
AfterFix_ScrollOffset.mov

@dotnet-policy-service dotnet-policy-service Bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Apr 29, 2025
@jsuarezruiz jsuarezruiz added area-controls-collectionview CollectionView, CarouselView, IndicatorView platform/android labels Apr 30, 2025
@jsuarezruiz

Copy link
Copy Markdown
Contributor

/azp run MAUI-UITests-public

@jsuarezruiz jsuarezruiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The test KeepScrollOffset is failing on Android:

 at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2420
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2437
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 680
   at Microsoft.Maui.TestCases.Tests.Issues.CollectionViewItemsUpdatingScrollModeUITests.KeepScrollOffset() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs:line 51
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

@prakashKannanSf3972

Copy link
Copy Markdown
Contributor Author

The test KeepScrollOffset is failing on Android:

 at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2420
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2437
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 680
   at Microsoft.Maui.TestCases.Tests.Issues.CollectionViewItemsUpdatingScrollModeUITests.KeepScrollOffset() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs:line 51
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

@jsuarezruiz, Since the existing test logic did not align with the expected KeepScrollOffset behavior, I have updated the test accordingly. The latest changes properly validate the expected output and are expected to pass in the next CI run.

@jsuarezruiz

Copy link
Copy Markdown
Contributor

/azp run MAUI-UITests-public

@prakashKannanSf3972
prakashKannanSf3972 marked this pull request as ready for review May 2, 2025 11:06
@prakashKannanSf3972
prakashKannanSf3972 requested a review from a team as a code owner May 2, 2025 11:06
{
items = new ObservableCollection<string>(Enumerable.Range(1, 30).Select(i => $"Item {i}"));

Button keepScrollOffsetButton = CreateButton("KeepScrollOffset", "KeepScrollOffsetButton", OnKeepScrollOffsetClicked);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could include more Buttons to change the ItemsUpdatingScrollMode value https://github.com/dotnet/maui/blob/main/src/Controls/src/Core/Items/ItemsUpdatingScrollMode.cs#L8
and test the behavior with the different possibilities?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jsuarezruiz,

Yes, adding tests for the various ItemsUpdatingScrollMode values would provide broader coverage. However,I would like to highlight a couple of existing platform-specific issues currently impacting these modes:

On Android: The KeepItemsInView mode currently does not work as expected, which is a known issue (#29145). This is being addressed in PR #27153, which is still under review.

On iOS : KeepLastItemInView is also not functioning correctly. This is another known issue (28716), which is also addressed in the same PR currently under review: PR #28720

Given these inconsistencies, only the KeepScrollOffset mode behaves consistently across all platforms at this time. This PR focuses on verifying that stable behavior.

Since the other two PRs handles the KeepItemsInView mode and KeepLastItemInView mode can we avoid adding tests for those modes?

Looking for your insights.

@kubaflo

kubaflo commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

🤖 AI Summary

📊 Expand Full Review
🔍 Pre-Flight — Context & Validation
📝 Review SessionUpdated-Test · f951e4d

PR: #29255 - [Android] Fix KeepScrollOffset Behavior During Dynamic Item Additions in CollectionView
Issue: #29131 - [Android] KeepScrollOffset doesn't work as expected when new items are added in CollectionView
Author: prakashKannanSf3972 (Syncfusion partner)
Platforms Affected: Android only (labels: platform/android, area-controls-collectionview)

Issue Summary

On Android, the ItemsUpdatingScrollMode.KeepScrollOffset property on CollectionView fails to maintain the current scroll position when new items are inserted at the beginning of the collection. The view jumps upward instead of staying at the same visual position.

Files Changed

Fix files:

  • src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs (+9)
  • src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs (+31, -10)

Test files:

  • src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs (+85, new file)
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs (+31, new file)
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs (+7, -2)

PR Discussion Summary

File:Line Reviewer Says Author Says Status
Issue29131.cs:17 Add buttons to test all ItemsUpdatingScrollMode values KeepItemsInView has known issues (#29145, PR #27153); KeepLastItemInView also broken on iOS (#28716, PR #28720). Only KeepScrollOffset stable. ⚠️ UNRESOLVED
KeepScrollOffset test Test failing on Android with TimeoutException Updated test logic to properly validate expected KeepScrollOffset behavior Addressed

Reviewer (jsuarezruiz) requested changes due to failing test; author updated the tests.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #29255 Move ScrollListener setup to UpdateItemsUpdatingScrollMode(); add isFirstItemReached flag to gate scroll correction to top-position-only ⏳ PENDING (Gate) MauiRecyclerView.cs, ScrollHelper.cs Original PR

Key Technical Observations

  1. Logic change: Old code applied ScrollBy(-delta) unconditionally; new code only applies it when isFirstItemReached is true (i.e., offset == 0)
  2. Listener management: Old code added listener lazily (on first UndoNextScrollAdjustment()); new code adds it eagerly when mode is set
  3. Potential ordering issue: In adapter update path (lines 363→366), UpdateItemsUpdatingScrollMode() adds ScrollHelper listener, then AddOrUpdateScrollListener() calls ClearOnScrollListeners() which removes ALL listeners including ScrollHelper's. May cause KeepScrollOffset to not work if mode is set before adapter update.
  4. Style issue: isFirstItemReached uses camelCase without underscore prefix, inconsistent with existing _undoNextScrollAdjustment style
  5. Missing newlines: Both new Issue29131.cs files lack newline at end of file

🚦 Gate — Test Verification
📝 Review SessionUpdated-Test · f951e4d

Result: ❌ INCONCLUSIVE (environment blocker)
Platform: android
Mode: Full Verification (attempted)

Test Runs

Run Expected Actual Notes
Tests WITHOUT fix FAIL FAIL (ADB0010) ADB install error - "Broken Pipe (32)" during build/install
Tests WITH fix PASS FAIL (TimeoutException) NavigateToIssue timed out waiting for GoToTestButton
Retry WITH fix PASS FAIL (TimeoutException) Same infrastructure failure

Root Cause of Gate Failure

The failure is infrastructure-related, NOT a fix code failure:

  • Without fix run: Build failed at install phase (ADB0010: Broken Pipe (32)) - never reached test assertions
  • With fix runs (x2): Build succeeded, app launched, but NavigateToIssue() timed out waiting for GoToTestButton on the main app page

The GoToTestButton timeout indicates the app was in an unexpected state (likely on the Issue29131 page from a previous partial run due to Fast Deploy state preservation). After retry, the same failure occurred.

Assessment

Gate failure is classified as environment blocker (retried once, same result). The fix code itself was not validated by automated tests, but code analysis reveals it is structurally reasonable.

Code Quality Issues Found (from static analysis)

  1. Listener ordering bug: In the adapter update path (UpdateAdapterUpdateItemsUpdatingScrollMode at line 363 → AddOrUpdateScrollListener at line 366), ClearOnScrollListeners() removes the ScrollHelper listener added by AddScrollListener(). This could cause KeepScrollOffset to silently fail when the adapter is refreshed.
  2. Naming convention: isFirstItemReached should be _isFirstItemReached per codebase style
  3. Missing EOF newline: Both Issue29131.cs files lack newline at end of file
  4. Existing test changed: CollectionViewUITests.KeepScrollOffset test assertion changed from "photo.jpg, 9" (1 item) to "FlowerBuds.jpg, 12" (5 items) without clear documentation of why the expected behavior changed

🔧 Fix — Analysis & Comparison
📝 Review SessionUpdated-Test · f951e4d

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #29255 Move listener management to UpdateItemsUpdatingScrollMode(); add isFirstItemReached flag to gate scroll correction ⚠️ BLOCKED (env issue) MauiRecyclerView.cs, ScrollHelper.cs NavigateToIssue infrastructure failure

Exhausted: No - skipped due to environment blocker (NavigateToIssue failure prevents test validation; retried once per rules)
Selected Fix: PR's fix (only candidate; Gate blocked by environment, not fix code)

Environment Blocker

try-fix phase was skipped per SHARED-RULES: "If try-fix cannot run due to environment issues after one retry, skip remaining try-fix models and proceed to Report." The Gate environment blocker (NavigateToIssue timeout) prevents meaningful try-fix validation as well.


📋 Report — Final Recommendation
📝 Review SessionUpdated-Test · f951e4d

⚠️ Final Recommendation: REQUEST CHANGES

Summary

PR #29255 fixes KeepScrollOffset mode in Android CollectionView where scroll position jumps when items are inserted at the top. The fix approach is reasonable but has one definite bug in the listener lifecycle that needs addressing before merge.

Gate testing was inconclusive due to an environment blocker (NavigateToIssue navigation timeout after a prior failed test run); the fix code itself was not empirically validated.


Root Cause Analysis

When items are inserted at the top of a RecyclerView using KeepScrollOffset mode:

  • If scrolled to middle/end: Android's LinearLayoutManager naturally adjusts the scroll offset to maintain visual position. No correction needed.
  • If at offset (0,0): RecyclerView may or may not auto-adjust; the ScrollBy(-delta) correction handles this case.

The original code applied ScrollBy(-delta) unconditionally, which interfered with RecyclerView's natural adjustment when scrolled down. The PR correctly gates the correction to isFirstItemReached (offset == 0).


Fix Quality Assessment

✅ Correct approach overall: The isFirstItemReached conditional logic is reasonable. When not at the top, RecyclerView's natural behavior maintains position; when at the top, manual correction is needed.

❌ Critical ordering bug in UpdateItemsSource():

public virtual void UpdateItemsSource()
{
    // ...
    UpdateItemsUpdatingScrollMode(); // line 363: adds ScrollHelper listener if KeepScrollOffset
    UpdateEmptyView();
    AddOrUpdateScrollListener();    // line 366: ClearOnScrollListeners() removes ALL listeners!
    // ...
}

AddOrUpdateScrollListener() calls ClearOnScrollListeners() which removes all registered listeners, including the ScrollHelper listener just added by UpdateItemsUpdatingScrollMode(). After UpdateItemsSource() runs, ScrollHelper._maintainingScrollOffsets = true but the listener is NOT actually registered. This means KeepScrollOffset silently stops working whenever ItemsSource is changed while the mode is active.

Fix: Either call UpdateItemsUpdatingScrollMode() AFTER AddOrUpdateScrollListener(), or call ScrollHelper.AddScrollListener() again after ClearOnScrollListeners():

AddOrUpdateScrollListener();        // call first (clears then adds RecyclerViewScrollListener)
UpdateItemsUpdatingScrollMode();    // call after (won't be wiped by ClearOnScrollListeners)

Code Quality Issues

Issue Severity Details
Listener ordering bug in UpdateItemsSource Critical ClearOnScrollListeners() wipes ScrollHelper listener added by UpdateItemsUpdatingScrollMode()
isFirstItemReached naming Minor Should be _isAtTopOffset or _isScrolledToTop per convention (_ prefix, meaningful name)
Indentation on line 219-220 Minor isFirstItemReached = newXOffset == 0 / && newYOffset == 0 - && should be indented consistently
Missing newline at end of files Minor Both Issue29131.cs files (HostApp and TestCases) lack newline at EOF
Existing test semantics change Needs clarification KeepScrollOffset test changed from "add 1 item → expect photo.jpg, 9" to "add 5 items → expect FlowerBuds.jpg, 12". Why was the expected element changed?

Test Analysis

  • New test (Issue29131.cs): Well-structured, covers alternating scroll-to-end/start + insert scenarios. Could not be validated due to Gate infrastructure issue.
  • Existing test modification: The KeepScrollOffset test in CollectionViewUITests was changed. The old expected behavior ("photo.jpg, 9" after 1 insert) was apparently wrong; the new behavior ("FlowerBuds.jpg, 12" after 5 inserts) should be documented.

Title & Description

  • Title: Minor style issue: [Android] - should be [Android] (extra dash unnecessary)
  • Description: Good structure; could clarify the isFirstItemReached logic and why it's the correct approach

Requested Changes

  1. [Required] Fix listener ordering in UpdateItemsSource() - move UpdateItemsUpdatingScrollMode() call to AFTER AddOrUpdateScrollListener() (or re-add ScrollHelper listener after clearing)
  2. [Required] Add comment in TrackOffsets() explaining WHY correction is conditional on isFirstItemReached (it's a non-obvious design decision)
  3. [Recommended] Rename isFirstItemReached to _isAtTopOffset for clarity and consistency
  4. [Recommended] Add newline at end of both Issue29131.cs files
  5. [Optional] Update PR title to remove extra dash: [Android] Fix KeepScrollOffset Behavior...

📋 Expand PR Finalization Review
Title: ✅ Good

Current: [Android] - Fix KeepScrollOffset Behavior During Dynamic Item Additions in CollectionView

Description: ✅ Good
  • Extra dash after [Android] is non-standard (convention is [Android] Fix... not [Android] - Fix...)
  • "Dynamic Item Additions" is somewhat verbose

✨ Suggested PR Description

[!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 if this change resolves your issue. Thank you!

Root Cause

When ItemsUpdatingScrollMode is set to KeepScrollOffset, the ScrollHelper previously registered its scroll listener lazily inside UndoNextScrollAdjustment() — only when the first adjustment was about to be undone. This lazy registration caused a race condition: the listener would miss the initial scroll events that captured the pre-insertion offsets, leading to incorrect delta values and unintended upward scrolling after items were inserted at the top of the CollectionView.

Additionally, TrackOffsets() applied the scroll correction unconditionally, which caused spurious corrections when the view was already at a scroll offset of zero.

Description of Change

Two changes in ScrollHelper.cs:

  1. Moved scroll listener management to UpdateItemsUpdatingScrollMode — Added AddScrollListener() and RemoveScrollListener() methods to ScrollHelper and call them from MauiRecyclerView.UpdateItemsUpdatingScrollMode(). The listener is now registered eagerly when KeepScrollOffset mode is set (and unregistered when switching away from it), rather than lazily on the first adjustment.

  2. Guard scroll correction with position check — Added an _isFirstItemReached flag that tracks whether the scroll position has been at offset (0, 0). In TrackOffsets(), the ScrollBy(-dx, -dy) correction is only applied when _isFirstItemReached is true (scroll position was at the start of the list), preventing spurious upward scrolling when the view is positioned in the middle or end of the list.

Also removed the listener registration code that was previously inside UndoNextScrollAdjustment() since it is now handled upstream.

Files changed:

  • src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs — core fix
  • src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs — calls AddScrollListener/RemoveScrollListener in mode update
  • src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs — new UI test page
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs — new NUnit UI test
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs — updated existing KeepScrollOffset test to add 5 items (was 1) to better validate the fix

Issues Fixed

Fixes #29131

Platforms Tested

  • Android
  • iOS (Android-only fix, no behavior change)
  • Windows (Android-only fix, no behavior change)
  • Mac (Android-only fix, no behavior change)
Code Review: ✅ Passed

Code Review — PR #29255

🟡 Suggestions


1. Naming Convention: isFirstItemReached doesn't follow class conventions

File: src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs

Problem: All other private fields in ScrollHelper use underscore prefix and camelCase:

bool _undoNextScrollAdjustment;
bool _maintainingScrollOffsets;
int _lastScrollX;
int _lastScrollY;
int _lastDeltaX;
int _lastDeltaY;

The new field breaks this convention:

bool isFirstItemReached = true;  // ❌ missing _ prefix, no access modifier

Recommendation:

bool _isFirstItemReached = true;  // ✅

2. Misleading Variable Name: isFirstItemReached

File: src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs

Problem: The name isFirstItemReached implies "the user scrolled to see the first item in the collection." However, its actual meaning is "the scroll offset is currently at (0, 0)" — the very beginning of the scrollable content. These are related but not identical concepts (the scroll could be at position 0 without the user intentionally "reaching" the first item).

A more descriptive name would clarify the intent:

bool _isAtScrollOrigin = true;  // ✅ clearer — tracks whether offset is (0,0)

Or if the intent is specifically to guard against repeated corrections:

bool _scrollOffsetResetSeen = true;  // ✅ more explicit about the guard purpose

This matters for future maintainers (and agents) trying to understand when the scroll correction applies.


3. Modified existing KeepScrollOffset test changed significantly without explanation

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs

Problem: The existing KeepScrollOffset test was changed from clicking AddItemAbove once to five times, and the expected visible element changed from "photo.jpg, 9" to "FlowerBuds.jpg, 12":

// Before:
App.Click("AddItemAbove");
App.WaitForElement("photo.jpg, 9");

// After:
for (int i = 0; i < 5; i++)
{
    App.Click("AddItemAbove");
}
App.WaitForElement("FlowerBuds.jpg, 12");

Questions raised:

  • Why does 5 additions instead of 1 better validate the fix?
  • Did the old expected element ("photo.jpg, 9") change because the fix altered what should be visible after one insertion?
  • Was the previous assertion wrong, or does the new behavior under the fix naturally land on "FlowerBuds.jpg, 12" after 5 insertions?

Recommendation: Add a comment explaining why 5 iterations and why the expected element changed. If the old assertion ("photo.jpg, 9") was previously failing due to this bug, that's worth documenting.


4. Missing newline at end of file

Files:

  • src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs

Both files are missing a trailing newline (the diff shows \ No newline at end of file). This is a minor style issue but inconsistent with the rest of the codebase.

Recommendation: Add a newline at the end of both files.


✅ Looks Good

  • Listener lifecycle management — Moving AddScrollListener/RemoveScrollListener to UpdateItemsUpdatingScrollMode is architecturally correct: listener registration should be tied to mode changes, not to the first scroll event. The old lazy approach in UndoNextScrollAdjustment was fragile.

  • Guard idiom in AddScrollListener/RemoveScrollListener — The if (!_maintainingScrollOffsets) / if (_maintainingScrollOffsets) guards prevent double-registration and double-removal. Correct.

  • New test file Issue29131.cs — The HostApp page and NUnit test are well-structured. Uses TestContentPage base class, proper AutomationIds, [Category(UITestCategories.CollectionView)], and the test exercises the KeepScrollOffset behavior from multiple scroll positions.

  • Platform tagging — The HostApp [Issue] attribute correctly marks PlatformAffected.Android, matching the Android-only fix.


@kubaflo kubaflo added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 8, 2026

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like the test is failing

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds coverage and an Android handler change intended to address ItemsUpdatingScrollMode.KeepScrollOffset behavior when items are inserted into a CollectionView, plus updates an existing CollectionView scroll-mode UI test.

Changes:

  • Add new HostApp issue page + UITest for GitHub issue 29131 (KeepScrollOffset + inserting items).
  • Update existing CollectionViewItemsUpdatingScrollMode UITest assertion logic.
  • Modify Android ScrollHelper / MauiRecyclerView to manage scroll-offset tracking via a scroll listener.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs New Appium UITest for issue 29131 scenario.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CollectionViewUITests.CollectionViewItemsUpdatingScrollMode.cs Adjusts KeepScrollOffset test to perform multiple insertions + new expected element.
src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs New HostApp repro page for issue 29131 (CollectionView + buttons to scroll/insert).
src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs Changes KeepScrollOffset offset-tracking logic; adds explicit add/remove listener helpers.
src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs Hooks ScrollHelper listener lifecycle to ItemsUpdatingScrollMode changes.
Comments suppressed due to low confidence (1)

src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs:20

  • isFirstItemReached is initialized to true and only updated when _undoNextScrollAdjustment is false. That means the first item update after enabling KeepScrollOffset may incorrectly run the undo path even when the user is not at offset 0, because the flag hasn't been computed from the current scroll offsets yet. Consider initializing this flag based on the current computed offsets when starting tracking (or removing it and directly checking offsets).
		bool _isAtScrollOrigin = true;
		int _lastScrollX;
		int _lastScrollY;
		int _lastDeltaX;
		int _lastDeltaY;

Comment on lines +375 to +382
if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset)
{
ScrollHelper.AddScrollListener();
}
else
{
ScrollHelper.RemoveScrollListener();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed the concern

{
public Issue29131(TestDevice device) : base(device) { }

public override string Issue => "Android - KeepScrollOffset doesn't not works as expected when new items are added in CollectionView";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed the concern


[Test]
[Category(UITestCategories.CollectionView)]
public void KeepScrollOffSetShouldWork()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed the concern


namespace Controls.TestCases.HostApp.Issues;

[Issue(IssueTracker.Github, 29131, "Android - KeepScrollOffset doesn't not works as expected when new items are added in CollectionView", PlatformAffected.Android)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed the concern

@HarishwaranVijayakumar

Copy link
Copy Markdown
Contributor

Looks like the test is failing

@kubaflo, I ensured the test passed locally and also addressed the valid AI concerns.

@sheiksyedm sheiksyedm added the s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation label Mar 11, 2026
@MauiBot MauiBot added s/agent-review-incomplete s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-review-incomplete labels Mar 29, 2026

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please review the AI's summary?

@kubaflo

kubaflo commented May 26, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 2 findings

See inline comments for details.


_undoNextScrollAdjustment = false;
_recyclerView.ScrollBy(-_lastDeltaX, -_lastDeltaY);
if (_isAtScrollOrigin)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[major] Android CollectionView — KeepScrollOffset semantics — This gates the offset correction on _isAtScrollOrigin, so once the user is scrolled away from offset 0, inserts before the viewport are allowed to keep RecyclerView's automatic shifted offset instead of restoring the previous absolute pixel offset. Concrete scenario: set ItemsUpdatingScrollMode=KeepScrollOffset, scroll down, insert an item at index 0; RecyclerView increases ComputeVerticalScrollOffset() to keep the old item visible, but this branch skips ScrollBy(-_lastDeltaY), so the absolute scroll offset is not preserved. KeepScrollOffset should undo the adapter-induced delta regardless of whether the current offset is origin.


if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset)
{
ScrollHelper.AddScrollListener();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[major] Android CollectionView — scroll listener lifecycleScrollHelper.AddScrollListener() can no-op after the helper has already been removed by AddOrUpdateScrollListener()/RemoveScrollListener(), because those methods call ClearOnScrollListeners() but do not reset ScrollHelper's _maintainingScrollOffsets flag. Concrete scenario: with KeepScrollOffset active, UpdateItemsSource() calls AddOrUpdateScrollListener(), clearing all listeners including ScrollHelper; then this line calls AddScrollListener(), but _maintainingScrollOffsets is still true so the helper is not re-registered and future collection updates are not tracked.

@MauiBot MauiBot removed the s/agent-changes-requested AI agent recommends changes - found a better alternative or issues label May 26, 2026
@MauiBot

This comment has been minimized.

@kubaflo

kubaflo commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/enhanced-reviewer -p android

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 3 findings

See inline comments for details.


_undoNextScrollAdjustment = false;
_recyclerView.ScrollBy(-_lastDeltaX, -_lastDeltaY);
if (_isAtScrollOrigin)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[major] CollectionView Android — Gating the offset correction on _isAtScrollOrigin changes KeepScrollOffset semantics when the user is already scrolled away from zero. In that scenario, inserting items above lets RecyclerView keep the same visible item anchored instead of restoring the absolute scroll offset, which matches KeepItemsInView more than the documented KeepScrollOffset behavior.


if (ItemsView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset)
{
ScrollHelper.AddScrollListener();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[major] Handler lifecycleScrollHelper.AddScrollListener() can no-op after AddOrUpdateScrollListener() calls ClearOnScrollListeners(). ClearOnScrollListeners() removes the helper from RecyclerView, but _maintainingScrollOffsets remains true, so changing/replacing the ItemsSource while in KeepScrollOffset mode can leave the helper unregistered and future offset tracking disabled.

{
int index = (count % 2 == 0) ? 0 : items.Count - 1;
var position = (count % 2 == 0) ? ScrollToPosition.Start : ScrollToPosition.End;
collectionView.ScrollTo(index, position: position, animate: true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[moderate] UI test reliability — The page starts an animated ScrollTo and the test immediately inserts an item. On slower Android devices the insert can race the still-running scroll animation, making the test assert the animation timing rather than the CollectionView update behavior. Prefer a non-animated scroll or a deterministic wait for the target item before enabling insertion.

@MauiBot MauiBot added s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR labels Jun 6, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI Review Summary

@prakashKannanSf3972 — new AI review results are available based on this last commit: f1750ff.
Address AI summary To request a fresh review after new comments or commits, comment /review rerun.

Gate Passed Code Review In Review Confidence High Platform Android

Review Sessions — click to expand
Gate — Test Before & After Fix

Gate Result: ✅ PASSED

Platform: ANDROID · Base: main · Merge base: e904e900

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ CollectionViewItemsUpdatingScrollModeUITests CollectionViewItemsUpdatingScrollModeUITests ✅ FAIL — 1297s ✅ PASS — 547s
🖥️ Issue29131 Issue29131 ✅ FAIL — 683s ✅ PASS — 501s
🔴 Without fix — 🖥️ CollectionViewItemsUpdatingScrollModeUITests: FAIL ✅ · 1297s

(truncated to last 15,000 chars)

ntPtr*)


  Standard Error Messages:
 >>>>> 06/06/2026 20:45:03 The FixtureSetup threw an exception. Attempt 0/1.
 Exception details: System.TimeoutException: CollectionView ItemsUpdatingScrollMode
    at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
    at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
    at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
    at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
    at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
    at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576
 >>>>> 06/06/2026 20:45:21 The FixtureSetup threw an exception. Attempt 1/1.
 Exception details: System.TimeoutException: CollectionView ItemsUpdatingScrollMode
    at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
    at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
    at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
    at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
    at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
    at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576

>>>>> 06/06/2026 20:45:37 The FixtureSetup threw an exception. Attempt 0/1.
Exception details: System.TimeoutException: Support for KeepLastItemInView for CV2
   at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
   at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576
>>>>> 06/06/2026 20:45:39 FixtureSetup for Issue28716(Android)
>>>>> 06/06/2026 20:45:55 The FixtureSetup threw an exception. Attempt 1/1.
Exception details: System.TimeoutException: Support for KeepLastItemInView for CV2
   at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
   at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576
>>>>> 06/06/2026 20:45:55 Log types: logcat, bugreport, server
>>>>> 06/06/2026 20:45:55 Log types: logcat, bugreport, server
Setup failed for test fixture Microsoft.Maui.TestCases.Tests.Issues.Issue28716(Android)
System.TimeoutException : Support for KeepLastItemInView for CV2
StackTrace:    at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
   at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576
   at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 221
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
>>>>> 06/06/2026 20:45:56 FixtureSetup for Issue29131(Android)
  Failed KeepLastItemInViewShouldWork [33 s]
  Error Message:
   OneTimeSetUp: System.TimeoutException : Support for KeepLastItemInView for CV2
  Stack Trace:
     at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
   at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576
   at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 221
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)

>>>>> 06/06/2026 20:46:12 The FixtureSetup threw an exception. Attempt 0/1.
Exception details: System.TimeoutException: Android - KeepScrollOffset does not work as expected when new items are added in CollectionView
   at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
   at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576
>>>>> 06/06/2026 20:46:14 FixtureSetup for Issue29131(Android)
>>>>> 06/06/2026 20:46:30 The FixtureSetup threw an exception. Attempt 1/1.
Exception details: System.TimeoutException: Android - KeepScrollOffset does not work as expected when new items are added in CollectionView
   at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
   at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576
>>>>> 06/06/2026 20:46:30 Log types: logcat, bugreport, server
>>>>> 06/06/2026 20:46:30 Log types: logcat, bugreport, server
  Failed KeepScrollOffsetShouldWork [34 s]
  Error Message:
   OneTimeSetUp: System.TimeoutException : Android - KeepScrollOffset does not work as expected when new items are added in CollectionView
  Stack Trace:
     at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
   at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576
   at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 221
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Setup failed for test fixture Microsoft.Maui.TestCases.Tests.Issues.Issue29131(Android)
System.TimeoutException : Android - KeepScrollOffset does not work as expected when new items are added in CollectionView
StackTrace:    at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.NavigateToIssue(String issue) in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 54
   at Microsoft.Maui.TestCases.Tests._IssuesUITest.TryToResetTestState() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/_IssuesUITest.cs:line 25
   at Microsoft.Maui.TestCases.Tests.UITest.FixtureSetup() in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 576
   at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 221
   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
Results File: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/retry-CollectionViewItemsUpdatingScrollModeUITests.trx

Test Run Failed.
Total tests: 17
     Failed: 17
 Total time: 6.7606 Minutes

🟢 With fix — 🖥️ CollectionViewItemsUpdatingScrollModeUITests: PASS ✅ · 547s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0-android36.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0-android36.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Maps -> /home/vsts/work/1/s/artifacts/bin/Maps/Debug/net10.0-android36.0/Microsoft.Maui.Maps.dll
  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.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Foldable.dll
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Xaml.dll
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Maps.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.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.80-ci+azdo.14304363
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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:06:34.33
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.80-ci+azdo.14304363
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.CustomAttributes -> /home/vsts/work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.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.80-ci+azdo.14304363
  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
  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.Appium -> /home/vsts/work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.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.
[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.12]   Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.34]   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 3 of 3 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 06/06/2026 21:05:59 KeepItemsInView Start
>>>>> 06/06/2026 21:05:59 FixtureSetup for KeepItemsInView
>>>>> 06/06/2026 21:06:25 KeepItemsInView Stop
  Passed KeepItemsInView [27 s]
>>>>> 06/06/2026 21:06:27 KeepLastItemInView Start
>>>>> 06/06/2026 21:06:27 FixtureSetup for KeepLastItemInView
>>>>> 06/06/2026 21:06:41 KeepLastItemInView Stop
  Passed KeepLastItemInView [16 s]
>>>>> 06/06/2026 21:06:43 KeepScrollOffset Start
>>>>> 06/06/2026 21:06:43 FixtureSetup for KeepScrollOffset
>>>>> 06/06/2026 21:06:59 KeepScrollOffset Stop
  Passed KeepScrollOffset [17 s]
NUnit Adapter 4.5.0.0: Test execution complete
Results File: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/CollectionViewItemsUpdatingScrollModeUITests.trx

Test Run Successful.
Total tests: 3
     Passed: 3
 Total time: 1.1769 Minutes
>>> TRX_RESULT_FILE: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/CollectionViewItemsUpdatingScrollModeUITests.trx

🔴 Without fix — 🖥️ Issue29131: FAIL ✅ · 683s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0-android36.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0-android36.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Maps -> /home/vsts/work/1/s/artifacts/bin/Maps/Debug/net10.0-android36.0/Microsoft.Maui.Maps.dll
  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.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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
  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
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Maps.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:09:02.46
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.80-ci+azdo.14304363
  Controls.CustomAttributes -> /home/vsts/work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  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.
[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.12]   Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.34]   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 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 06/06/2026 20:57:28 FixtureSetup for Issue29131(Android)
>>>>> 06/06/2026 20:57:29 KeepScrollOffsetShouldWork Start
>>>>> 06/06/2026 20:57:51 KeepScrollOffsetShouldWork Stop
>>>>> 06/06/2026 20:57:51 Log types: logcat, bugreport, server
  Failed KeepScrollOffsetShouldWork [22 s]
  Error Message:
   System.TimeoutException : Timed out waiting for element...
  Stack Trace:
     at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2757
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2784
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 793
   at Microsoft.Maui.TestCases.Tests.Issues.Issue29131.KeepScrollOffsetShouldWork() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29131.cs:line 23
   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
Results File: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue29131.trx

Total tests: 1
     Failed: 1
Test Run Failed.
 Total time: 37.4899 Seconds
>>> TRX_RESULT_FILE: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue29131.trx

🟢 With fix — 🖥️ Issue29131: PASS ✅ · 501s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0-android36.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0-android36.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Maps -> /home/vsts/work/1/s/artifacts/bin/Maps/Debug/net10.0-android36.0/Microsoft.Maui.Maps.dll
  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.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Xaml.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.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Maps.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Foldable.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.80-ci+azdo.14304363
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.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.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Xaml.dll
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Foldable.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:06:36.61
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.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.80-ci+azdo.14304363
  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.80-ci+azdo.14304363
  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
  UITest.NUnit -> /home/vsts/work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
  VisualTestUtils -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.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.
[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.18]   Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.38]   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 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 06/06/2026 21:15:09 FixtureSetup for Issue29131(Android)
>>>>> 06/06/2026 21:15:10 KeepScrollOffsetShouldWork Start
>>>>> 06/06/2026 21:15:21 KeepScrollOffsetShouldWork Stop
  Passed KeepScrollOffsetShouldWork [11 s]
NUnit Adapter 4.5.0.0: Test execution complete
Results File: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue29131.trx

Test Run Successful.
Total tests: 1
     Passed: 1
 Total time: 22.8361 Seconds
>>> TRX_RESULT_FILE: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue29131.trx

📁 Fix files reverted (3 files)
  • eng/pipelines/ci-copilot.yml
  • src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs
  • src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs

UI Tests — CollectionView

Detected UI test categories: CollectionView


Pre-Flight — Context & Validation

Issue: #29131 - [Android] KeepItemsInView and KeepScrollOffset doesn't not works as expected when new items are added in CollectionView
PR: #29255 - [Android] - Fix KeepScrollOffset Behavior During Dynamic Item Additions in CollectionView
Platforms Affected: Android
Files Changed: 2 implementation, 3 test

Key Findings

  • Issue #29131 reports Android CollectionView ItemsUpdatingScrollMode.KeepScrollOffset failing when new items are inserted at the top.
  • PR #29255 fixes the observed gate scenario by registering ScrollHelper early and only undoing RecyclerView offset shifts when the view is at origin.
  • Code review found the PR likely changes documented KeepScrollOffset semantics and has a listener lifecycle risk when ClearOnScrollListeners() removes ScrollHelper.
  • GitHub CLI authentication was unavailable, so PR/issue context was gathered via unauthenticated GitHub API reads and local branch diff.

Code Review Summary

Verdict: NEEDS_CHANGES
Confidence: high
Errors: 2 | Warnings: 1 | Suggestions: 0

Key code review findings:

  • src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs:208 - origin-only correction changes KeepScrollOffset toward KeepItemsInView behavior.
  • src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs:386 - ClearOnScrollListeners() can remove ScrollHelper while _maintainingScrollOffsets remains true.
  • ⚠️ src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs:77 - animated ScrollTo can race immediate item insertion in the UI test.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #29255 Persistent ScrollHelper listener plus origin-only offset undo ✅ PASSED (Gate) MauiRecyclerView.cs, ScrollHelper.cs, UI tests Original PR; gate result supplied by caller

Code Review — Deep Analysis

Code Review — PR #29255

Independent Assessment

What this changes: Android CollectionView KeepScrollOffset now eagerly registers ScrollHelper as a RecyclerView scroll listener, tracks whether the view is at scroll origin, and only applies the compensating ScrollBy(-delta) when at origin. It also adds/updates UI tests for item insertion while scrolling.
Inferred motivation: Prevent Android CollectionView from jumping unexpectedly when items are inserted at the top.

Reconciliation with PR Narrative

Author claims: KeepScrollOffset should preserve scroll behavior during dynamic insertions by adjusting offsets only when the first item is reached.
Agreement/disagreement: The listener-management motivation matches the code. However, the implementation conflicts with the documented KeepScrollOffset contract: ItemsUpdatingScrollMode.KeepScrollOffset says the absolute scroll position remains fixed and different items may become visible.

Findings

❌ Error — KeepScrollOffset semantics are changed to "keep visible items"

src/Controls/src/Core/Handlers/Items/Android/ScrollHelper.cs:208

Gating the correction on _isAtScrollOrigin means that when the user is scrolled away from offset 0, RecyclerView's anchor adjustment is left in place. That keeps the same visible item stable, which is closer to KeepItemsInView, not KeepScrollOffset. The public API docs state KeepScrollOffset maintains the absolute scroll position.

❌ Error — ScrollHelper listener state can desynchronize after ClearOnScrollListeners

src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs:386

AddOrUpdateScrollListener() calls ClearOnScrollListeners(), which removes ScrollHelper too, but does not reset _maintainingScrollOffsets. A later ScrollHelper.AddScrollListener() can no-op while the native listener is no longer registered.

⚠️ Warning — New UI test can race animated scrolling

src/Controls/tests/TestCases.HostApp/Issues/Issue29131.cs:77

The test page uses ScrollTo(..., animate: true), but the test immediately inserts items after clicking the scroll button. On slower Android devices, insertion may happen before scrolling completes. Prefer non-animated scrolling or wait for a deterministic sentinel before insertion.

Devil's Advocate

The PR may match the issue author's desired visual behavior, but that behavior appears to contradict the enum's documented absolute-offset semantics. Existing bot comments already identified related listener/semantic concerns, but they still apply to the current diff.

Verdict: NEEDS_CHANGES

Confidence: high
Summary: The PR has correctness issues in both behavior and listener lifecycle. CI was not assessed via authenticated gh because this environment lacks GitHub CLI authentication.


Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix Post-layout absolute-offset restoration with RecyclerView.Post ⚠️ Blocked 2 files Build/deploy succeeded; UI test execution did not complete before stop
2 try-fix Pre-layout adapter metadata anchor with ScrollToPositionWithOffset ❌ Fail 3 files KeepScrollOffset and KeepScrollOffsetShouldWork timed out
3 try-fix Lazy one-shot ScrollHelper listener and targeted listener removal ❌ Fail 2 files Corrected compile error, then KeepScrollOffset tests timed out
4 try-fix Persistent listener with unconditional offset restoration and targeted listener removal ❌ Fail 2 files Addresses review concerns but fails current KeepScrollOffset expectations
PR PR #29255 Persistent listener plus origin-only offset undo ✅ PASSED (Gate) 2 implementation files + tests Original PR; supplied gate result says tests fail without fix and pass with fix

Cross-Pollination

Model Round New Ideas? Details
maui-expert-reviewer 1 Yes Candidate 1: post-layout absolute-offset restoration
maui-expert-reviewer 2 Yes Candidate 2: adapter metadata/pre-layout anchoring after Candidate 1 was blocked
maui-expert-reviewer 3 Yes Candidate 3: lazy one-shot listener after Candidate 2 failed
maui-expert-reviewer 4 Yes Candidate 4: persistent listener with semantic/lifecycle fixes after Candidate 3 failed

Exhausted: Yes
Selected Fix: PR's fix — It is the only fix with a passing gate result. No alternative candidate both passed the Android UI tests and was demonstrably better than the PR's current implementation.


Report — Final Recommendation

Comparative Candidate Report — PR #29255

Ranking

Rank Candidate Regression result Assessment
1 pr ✅ Passed gate Only candidate with confirmed Android gate success. It fixes the observed Issue29131 scenario but retains major review concerns around documented KeepScrollOffset semantics and listener lifecycle.
2 try-fix-1 ⚠️ Blocked Post-layout absolute-offset restoration is conceptually promising and avoids the PR's origin gate, but the Android UI test run did not complete, so it cannot outrank a passing candidate.
3 pr-plus-reviewer ❌ Failed by equivalence to try-fix-4 implementation-side changes Applies all expert feedback, including unconditional offset restoration and targeted listener removal. Those implementation changes match try-fix-4, which failed the Android KeepScrollOffset regression tests, so this candidate must rank below passing or non-failing candidates.
4 try-fix-4 ❌ Failed Persistent early listener plus semantic/lifecycle fixes addressed the expert concerns, but KeepScrollOffset and KeepScrollOffsetShouldWork failed and failed again on retry.
5 try-fix-2 ❌ Failed Pre-layout adapter metadata anchoring passed unrelated modes but failed both KeepScrollOffset tests; anchoring by numeric adapter position was insufficient.
6 try-fix-3 ❌ Failed Lazy one-shot listener plus targeted removal was too narrow; the helper was not registered early/long enough to observe RecyclerView's offset delta.

Candidate comparison

pr: Best empirical result. The supplied gate says tests fail without the PR and pass with it on Android. The downside is real: the expert reviewer found that origin-only correction changes the documented absolute-offset semantics and that broad listener clearing can desynchronize ScrollHelper's native registration state.

pr-plus-reviewer: Best code-review posture but not best empirical posture. Applying all reviewer feedback necessarily includes the semantic/lifecycle changes tested by try-fix-4; that candidate failed the Android regression tests, so pr-plus-reviewer must be ranked lower than the raw PR despite addressing the review findings.

try-fix-1: Uses a different post-layout absolute-offset restoration strategy and may resolve the semantic concern without relying on a persistent listener. It was blocked rather than failed, but the requested ranking rule only guarantees failed regression candidates are below passed candidates; with no completed pass, it cannot beat pr.

try-fix-2: Adds adapter-change metadata and anchors before layout with ScrollToPositionWithOffset, but test evidence showed the viewport ended at the wrong content after insertions.

try-fix-3: Reduces listener lifetime and removes only owned listeners, but failed because the listener was active too late or too briefly for RecyclerView's update/layout scroll callbacks.

try-fix-4: Directly addresses the expert semantic and listener-lifecycle findings while keeping the PR's persistent early listener model, but failed both KeepScrollOffset regressions.

Winner

Winner: pr

The raw PR fix is the single winning candidate because it is the only candidate with confirmed passing Android gate results. This is not a clean merge recommendation: the expert findings should be surfaced as inline comments, but every candidate that applied those implementation fixes failed or lacked completed validation.


Future Action — review latest findings

No alternative fix was selected for this run. Review the session findings and CI results before merging.

@kubaflo
kubaflo changed the base branch from main to inflight/current June 7, 2026 11:29
@kubaflo
kubaflo merged commit a89b4de into dotnet:inflight/current Jun 7, 2026
173 of 177 checks passed
@github-actions github-actions Bot added this to the .NET 10.0 SR8 milestone Jun 7, 2026
@AdamEssenmacher

Copy link
Copy Markdown
Contributor

@kubaflo a couple of those last MauiBot comments look like real regressions to me. Is there an intent to follow-up in separate PR(s)? Or should new issues be opened?

@kubaflo

kubaflo commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@AdamEssenmacher here's the follow up PR: #27153 but you can open yours

PureWeen pushed a commit that referenced this pull request Jun 11, 2026
…ns in CollectionView (#29255)

> [!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 if this change resolves your issue.
Thank you!

### Issue Details 

`KeepScrollOffset` mode does not maintain scroll position when new items
are inserted into the CollectionView, resulting in unintended upward
scrolling.

### Root Cause 

When new items are added to the collection, the scroll offset is
recalculated based on the updated layout. This results in incorrect
offset adjustments, causing the scroll position to shift upward instead
of being correctly preserved.

### Description of Change

* Added `AddScrollListener` and `RemoveScrollListener` methods to manage
scroll listeners dynamically. Adjusted the `TrackOffsets` method to
ensure scroll offsets are only adjusted when the first item is reached,
ensuring accurate scroll behavior and preventing unintended upward
scrolling during dynamic item insertions.

### Issues Fixed

Fixes #29131 

* The Following [PR - 27153](#27153)
fixes the `KeepItemsInView` issue on Android.

**Tested the behaviour in the following platforms**

- [x] Android
- [x]  Windows
- [x]  iOS
- [x] Mac

### Output

| Before| After|
|--|--|
| <video
src="https://github.com/user-attachments/assets/5d258709-c128-4cc4-9a73-bf3ad32b1cc3">
| <video
src="https://github.com/user-attachments/assets/8b22544b-cbbd-4d97-a54a-4573ca0290d5">
|

---------
kubaflo pushed a commit that referenced this pull request Jun 17, 2026
…#29255 (#35946)

<!-- Please let the below note in for people that 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 if this change resolves your issue.
Thank you!
<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Issue Details

- Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset
stops working after ItemsSource is replaced.
- After the reassignment, inserting a new item at index 0 leaves the
previous top item anchored on screen and hides the new item above the
viewport — effectively KeepItemsInView semantics instead of
KeepScrollOffset.
- Only Android is affected. iOS behaves correctly.


### Root Cause of the issue
- MauiRecyclerView.RemoveScrollListener() calls
ClearOnScrollListeners(), which removes all scroll listeners registered
on the RecyclerView — including the one ScrollHelper self-registered.
- ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes
stale (true while the listener is actually detached).
- Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the
stale flag, so ScrollHelper is never re-attached.
- With no scroll callbacks reaching ScrollHelper, TrackOffsets() never
runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is
silently skipped.
### Before PR #29255
- ScrollHelper attached its listener lazily, inside
UndoNextScrollAdjustment(), only when an insert actually needed it.
- The stale-flag desync caused by ClearOnScrollListeners() was a latent
bug, but the lazy re-register path inside UndoNextScrollAdjustment()
masked it for the typical user — the listener got attached the next time
it was needed, so most KeepScrollOffset scenarios still worked.
### After PR #29255
- Listener registration was moved out of UndoNextScrollAdjustment() and
into an explicit ScrollHelper.AddScrollListener() method, called eagerly
from MauiRecyclerView.UpdateItemsUpdatingScrollMode().
- Both AddScrollListener() and the old lazy block use the same
_maintainingScrollOffsets guard, but the lazy self-heal block in
UndoNextScrollAdjustment() was deleted.
- Now, when ClearOnScrollListeners() detaches ScrollHelper's listener,
the flag stays true, the eager AddScrollListener() no-ops, the lazy
safety net is gone, and KeepScrollOffset is permanently broken after the
first ItemsSource reassignment.
### Description of Change

<!-- Enter description of the fix in this section -->
**Bug fix:**

* Fixed the scroll listener removal logic in `RemoveScrollListener()` by
removing only the specific listener instead of clearing all listeners,
which resolves the `KeepScrollOffset` issue when replacing the
`ItemsSource` in `CollectionView` on Android.
(`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`)

**Test coverage:**

* Added a new test case page that reproduces the issue and provides
buttons to replace the `ItemsSource`, scroll to the top, and insert
items at the top, making it easier to manually verify the fix.
(`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`)
* Introduced an automated UI test (Android only) that verifies the
`KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring
the regression is caught in the future.
(`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`)

### 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 #35806 

### Tested the behavior in the following platforms
 
- [ ] Windows
- [x] Android
- [ ] iOS
- [ ] Mac
 
### Output
 
| Before | After |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797">
| <video
src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a">
|







<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

---------

Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
@sheiksyedm sheiksyedm modified the milestones: .NET 10 SR8, .NET 10 SR9 Jun 18, 2026
PureWeen pushed a commit that referenced this pull request Jun 22, 2026
…ns in CollectionView (#29255)

> [!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 if this change resolves your issue.
Thank you!

### Issue Details 

`KeepScrollOffset` mode does not maintain scroll position when new items
are inserted into the CollectionView, resulting in unintended upward
scrolling.

### Root Cause 

When new items are added to the collection, the scroll offset is
recalculated based on the updated layout. This results in incorrect
offset adjustments, causing the scroll position to shift upward instead
of being correctly preserved.

### Description of Change

* Added `AddScrollListener` and `RemoveScrollListener` methods to manage
scroll listeners dynamically. Adjusted the `TrackOffsets` method to
ensure scroll offsets are only adjusted when the first item is reached,
ensuring accurate scroll behavior and preventing unintended upward
scrolling during dynamic item insertions.

### Issues Fixed

Fixes #29131 

* The Following [PR - 27153](#27153)
fixes the `KeepItemsInView` issue on Android.

**Tested the behaviour in the following platforms**

- [x] Android
- [x]  Windows
- [x]  iOS
- [x] Mac

### Output

| Before| After|
|--|--|
| <video
src="https://github.com/user-attachments/assets/5d258709-c128-4cc4-9a73-bf3ad32b1cc3">
| <video
src="https://github.com/user-attachments/assets/8b22544b-cbbd-4d97-a54a-4573ca0290d5">
|

---------
PureWeen pushed a commit that referenced this pull request Jun 22, 2026
…#29255 (#35946)

<!-- Please let the below note in for people that 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 if this change resolves your issue.
Thank you!
<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Issue Details

- Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset
stops working after ItemsSource is replaced.
- After the reassignment, inserting a new item at index 0 leaves the
previous top item anchored on screen and hides the new item above the
viewport — effectively KeepItemsInView semantics instead of
KeepScrollOffset.
- Only Android is affected. iOS behaves correctly.


### Root Cause of the issue
- MauiRecyclerView.RemoveScrollListener() calls
ClearOnScrollListeners(), which removes all scroll listeners registered
on the RecyclerView — including the one ScrollHelper self-registered.
- ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes
stale (true while the listener is actually detached).
- Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the
stale flag, so ScrollHelper is never re-attached.
- With no scroll callbacks reaching ScrollHelper, TrackOffsets() never
runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is
silently skipped.
### Before PR #29255
- ScrollHelper attached its listener lazily, inside
UndoNextScrollAdjustment(), only when an insert actually needed it.
- The stale-flag desync caused by ClearOnScrollListeners() was a latent
bug, but the lazy re-register path inside UndoNextScrollAdjustment()
masked it for the typical user — the listener got attached the next time
it was needed, so most KeepScrollOffset scenarios still worked.
### After PR #29255
- Listener registration was moved out of UndoNextScrollAdjustment() and
into an explicit ScrollHelper.AddScrollListener() method, called eagerly
from MauiRecyclerView.UpdateItemsUpdatingScrollMode().
- Both AddScrollListener() and the old lazy block use the same
_maintainingScrollOffsets guard, but the lazy self-heal block in
UndoNextScrollAdjustment() was deleted.
- Now, when ClearOnScrollListeners() detaches ScrollHelper's listener,
the flag stays true, the eager AddScrollListener() no-ops, the lazy
safety net is gone, and KeepScrollOffset is permanently broken after the
first ItemsSource reassignment.
### Description of Change

<!-- Enter description of the fix in this section -->
**Bug fix:**

* Fixed the scroll listener removal logic in `RemoveScrollListener()` by
removing only the specific listener instead of clearing all listeners,
which resolves the `KeepScrollOffset` issue when replacing the
`ItemsSource` in `CollectionView` on Android.
(`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`)

**Test coverage:**

* Added a new test case page that reproduces the issue and provides
buttons to replace the `ItemsSource`, scroll to the top, and insert
items at the top, making it easier to manually verify the fix.
(`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`)
* Introduced an automated UI test (Android only) that verifies the
`KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring
the regression is caught in the future.
(`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`)

### 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 #35806 

### Tested the behavior in the following platforms
 
- [ ] Windows
- [x] Android
- [ ] iOS
- [ ] Mac
 
### Output
 
| Before | After |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797">
| <video
src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a">
|







<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

---------

Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Jun 25, 2026
…ns in CollectionView (#29255)

> [!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 if this change resolves your issue.
Thank you!

### Issue Details 

`KeepScrollOffset` mode does not maintain scroll position when new items
are inserted into the CollectionView, resulting in unintended upward
scrolling.

### Root Cause 

When new items are added to the collection, the scroll offset is
recalculated based on the updated layout. This results in incorrect
offset adjustments, causing the scroll position to shift upward instead
of being correctly preserved.

### Description of Change

* Added `AddScrollListener` and `RemoveScrollListener` methods to manage
scroll listeners dynamically. Adjusted the `TrackOffsets` method to
ensure scroll offsets are only adjusted when the first item is reached,
ensuring accurate scroll behavior and preventing unintended upward
scrolling during dynamic item insertions.

### Issues Fixed

Fixes #29131 

* The Following [PR - 27153](#27153)
fixes the `KeepItemsInView` issue on Android.

**Tested the behaviour in the following platforms**

- [x] Android
- [x]  Windows
- [x]  iOS
- [x] Mac

### Output

| Before| After|
|--|--|
| <video
src="https://github.com/user-attachments/assets/5d258709-c128-4cc4-9a73-bf3ad32b1cc3">
| <video
src="https://github.com/user-attachments/assets/8b22544b-cbbd-4d97-a54a-4573ca0290d5">
|

---------
kubaflo pushed a commit that referenced this pull request Jun 25, 2026
…#29255 (#35946)

<!-- Please let the below note in for people that 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 if this change resolves your issue.
Thank you!
<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Issue Details

- Android CollectionView.ItemsUpdatingScrollMode = KeepScrollOffset
stops working after ItemsSource is replaced.
- After the reassignment, inserting a new item at index 0 leaves the
previous top item anchored on screen and hides the new item above the
viewport — effectively KeepItemsInView semantics instead of
KeepScrollOffset.
- Only Android is affected. iOS behaves correctly.


### Root Cause of the issue
- MauiRecyclerView.RemoveScrollListener() calls
ClearOnScrollListeners(), which removes all scroll listeners registered
on the RecyclerView — including the one ScrollHelper self-registered.
- ScrollHelper._maintainingScrollOffsets flag is never reset, so it goes
stale (true while the listener is actually detached).
- Subsequent ScrollHelper.AddScrollListener() calls short-circuit on the
stale flag, so ScrollHelper is never re-attached.
- With no scroll callbacks reaching ScrollHelper, TrackOffsets() never
runs and the ScrollBy(-delta) correction that drives KeepScrollOffset is
silently skipped.
### Before PR #29255
- ScrollHelper attached its listener lazily, inside
UndoNextScrollAdjustment(), only when an insert actually needed it.
- The stale-flag desync caused by ClearOnScrollListeners() was a latent
bug, but the lazy re-register path inside UndoNextScrollAdjustment()
masked it for the typical user — the listener got attached the next time
it was needed, so most KeepScrollOffset scenarios still worked.
### After PR #29255
- Listener registration was moved out of UndoNextScrollAdjustment() and
into an explicit ScrollHelper.AddScrollListener() method, called eagerly
from MauiRecyclerView.UpdateItemsUpdatingScrollMode().
- Both AddScrollListener() and the old lazy block use the same
_maintainingScrollOffsets guard, but the lazy self-heal block in
UndoNextScrollAdjustment() was deleted.
- Now, when ClearOnScrollListeners() detaches ScrollHelper's listener,
the flag stays true, the eager AddScrollListener() no-ops, the lazy
safety net is gone, and KeepScrollOffset is permanently broken after the
first ItemsSource reassignment.
### Description of Change

<!-- Enter description of the fix in this section -->
**Bug fix:**

* Fixed the scroll listener removal logic in `RemoveScrollListener()` by
removing only the specific listener instead of clearing all listeners,
which resolves the `KeepScrollOffset` issue when replacing the
`ItemsSource` in `CollectionView` on Android.
(`src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs`)

**Test coverage:**

* Added a new test case page that reproduces the issue and provides
buttons to replace the `ItemsSource`, scroll to the top, and insert
items at the top, making it easier to manually verify the fix.
(`src/Controls/tests/TestCases.HostApp/Issues/Issue35806.cs`)
* Introduced an automated UI test (Android only) that verifies the
`KeepScrollOffset` behavior after replacing the `ItemsSource`, ensuring
the regression is caught in the future.
(`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35806.cs`)

### 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 #35806 

### Tested the behavior in the following platforms
 
- [ ] Windows
- [x] Android
- [ ] iOS
- [ ] Mac
 
### Output
 
| Before | After |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/0b73f9e5-83e1-4e82-a382-47b22e56c797">
| <video
src="https://github.com/user-attachments/assets/fc2c4648-4d77-4b55-a988-0a87c2c17e8a">
|







<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

---------

Co-authored-by: KarthikRajaKalaimani <92777139+KarthikRajaKalaimani@users.noreply.github.com>
@kubaflo kubaflo mentioned this pull request Jul 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-controls-collectionview CollectionView, CarouselView, IndicatorView community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] KeepItemsInView and KeepScrollOffset doesn't not works as expected when new items are added in CollectionView

10 participants