CarouselView: Fix cascading PositionChanged/CurrentItemChanged events on collection update#31275
Conversation
|
Hey there @@praveenkumarkarunanithi! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
There was a problem hiding this comment.
Pull Request Overview
This PR fixes issues with CarouselView's event handling system where CurrentItemChangedEventArgs and PositionChangedEventArgs were not working properly on Windows and Android platforms. The fix prevents cascading position change events during collection updates by introducing an internal flag to disable animations during programmatic scrolls.
Key Changes:
- Added
_isInternalPositionUpdateflag to prevent cascading events during collection changes - Fixed position synchronization issues on Windows when items are added
- Disabled animations during collection updates to ensure single, clean position updates
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
Issue29529.cs (TestCases.Shared.Tests) |
Adds automated UI test to verify position and item change events fire correctly after item insertion |
Issue29529.cs (TestCases.HostApp) |
Creates test UI page with CarouselView demonstrating the issue and event tracking |
CarouselViewHandler.Windows.cs |
Implements Windows-specific fix with internal flag and position synchronization logic |
MauiCarouselRecyclerView.cs |
Implements Android-specific fix with internal flag and centralized scroll method |
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs
Outdated
Show resolved
Hide resolved
src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs
Show resolved
Hide resolved
Addressed all AI Agent concerns — corrected |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 31275Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 31275" |
🤖 AI Summary📊 Expand Full Review —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #31275 | Boolean flag _isInternalPositionUpdate to suppress scroll callbacks + disable animation during collection changes (all 3 platforms) |
✅ PASSED (Gate) | MauiCarouselRecyclerView.cs, CarouselViewHandler.Windows.cs, CarouselViewController2.cs |
Addresses both cascading events and animation-driven repeat events |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | Detach CarouselViewScrolled during collection change + set _gotoPosition to block UpdateFromCurrentItem from re-triggering ScrollTo |
✅ PASS | MauiCarouselRecyclerView.cs (Android only) |
Uses no new fields; leverages existing _gotoPosition and event subscribe/unsubscribe |
| 2 | try-fix (claude-sonnet-4.6) | _nextExpectedPosition guard in UpdatePosition — rejects spurious intermediate positions from RecyclerView scroll offset shifts |
✅ PASS | MauiCarouselRecyclerView.cs (Android only) |
Minimal: one new int field + one early-return guard |
| 3 | try-fix (gpt-5.3-codex) | Suppress non-user scroll callbacks during collection updates using existing _noNeedForScroll + IsDragging/IsScrolling state — no new fields |
✅ PASS | MauiCarouselRecyclerView.cs (Android only) |
Uses existing state, callback-level filtering |
| 4 | try-fix (gpt-5.4) | Reorder UpdatePosition before SetCurrentItem in dispatch callback |
❌ FAIL | MauiCarouselRecyclerView.cs |
RecyclerView compensation scroll fires independently of ordering |
| PR | PR #31275 | Boolean flag _isInternalPositionUpdate + try/finally, disables animation, bounds check (all 3 platforms) |
✅ PASSED (Gate) | MauiCarouselRecyclerView.cs, CarouselViewHandler.Windows.cs, CarouselViewController2.cs |
Only approach covering all 3 platforms |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | Yes | RecyclerView.SuppressLayout(true) to prevent OnScrolled at Android level |
| claude-sonnet-4.6 | 2 | Yes | Gate on RecyclerView.IsComputingLayout internal flag |
| gpt-5.3-codex | 2 | Yes | Update-generation token on pending syncs; ignore stale callbacks |
| gpt-5.4 | 2 | Yes | Capture centered item pixel offset before change, restore after |
Exhausted: Yes (4/4 models complete, cross-pollination done; new ideas are more complex than existing passes)
Selected Fix: PR #31275 — Only approach covering all 3 platforms (Android + Windows + iOS/Mac); try/finally robustness; self-documenting flag; handles both cascading event sources.
📋 Report — Final Recommendation
✅ Final Recommendation: APPROVE
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issue #29529, 3 platform impls, UI test |
| Gate | ✅ PASSED | Android — FAIL without fix, PASS with fix |
| Try-Fix | ✅ COMPLETE | 4 attempts: 3 PASS, 1 FAIL; cross-pollination exhausted |
| Report | ✅ COMPLETE |
Summary
PR #31275 fixes cascading PositionChanged/CurrentItemChanged events in CarouselView when items are inserted at index 0 with ItemsUpdatingScrollMode.KeepItemsInView. The fix spans all 3 platform implementations (Android via Items/, Windows via Items/, iOS/Mac via Items2/). Gate passed on Android. Three independent alternative approaches also passed tests, confirming the root cause is well-understood and the PR's approach is sound.
Selected Fix: PR #31275 (over all try-fix alternatives)
Root Cause
When an item is inserted at index 0, the platform scroll views (Android RecyclerView, WinUI ScrollViewer) automatically shift their scroll offset to compensate, keeping the currently-displayed item visible. This offset shift triggers the CarouselViewScrolled/OnScrollViewChanged callback, which calls SetCarouselViewPosition()/UpdatePosition(). Because this happens during — or immediately after — the collection-change processing, Position and CurrentItem are updated again with stale/intermediate values, causing PositionChanged and CurrentItemChanged to fire multiple times with wrong PreviousPosition values.
A secondary source on Android: calling ScrollTo() with animation during collection changes produces frame-by-frame scroll events that each trigger intermediate position updates.
Fix Quality
Strengths:
-
Cross-platform completeness: The only passing approach that fixes all 3 platforms. All 3 independent try-fix alternatives (Attempts 1-3) only fixed Android. The PR also fixes Windows (
CarouselViewHandler.Windows.cs) and iOS/Mac (CarouselViewController2.cs). -
Robustness via try/finally: The
_isInternalPositionUpdateflag is reset in afinallyblock, ensuring it is always cleared even if an exception occurs. This was explicitly requested by reviewerjsuarezruizand correctly implemented. -
Handles both cascading sources: (a) Suppresses
CarouselViewScrolledcallbacks via early-return when flag is set. (b) Disables animation viaScrollToItemPositionhelper so no animation-driven intermediate scroll positions are generated. -
Bounds check added: New
ScrollToItemPosition()helper validatesposition >= 0 && position < Countbefore callingScrollTo, preventing out-of-range scrolls (also requested by reviewer). -
iOS guard placement is correct:
_isInternalCollectionUpdateis cleared at the top ofCollectionViewUpdated(before anySetPosition/SetCurrentItemcalls within that method), so legitimate position updates inCollectionViewUpdatedare not suppressed — only spurious UIKit scroll callbacks during the batch update window are blocked.
Observations (non-blocking):
-
Android flag lifecycle:
_isInternalPositionUpdateis set totrueat the top ofCollectionItemsSourceChangedbefore theremovingAnyPreviousearly-exit check. The early-exit branch resets it synchronously (line 269). The dispatched callback'sfinallyresets it asynchronously (line 319). This creates a small window where rapid collection changes could interact. This is an acceptable trade-off and consistent with the approach used in the Windows implementation. -
Android test only: The UI test (
Issue29529VerifyPreviousPositionOnInsert) runs on Android (confirmed by Gate). Windows behavior relies on code review. The issue was originally Windows-only; Android exposure was found during PR development. -
Test assertions are precise: Verifies
CurrentPosition: 0, Previous Position: 3,Current Item: Item 0, Previous Item: Item 4, and event countsPositionChanged: 1, CurrentItemChanged: 1— directly validates the fix eliminates duplicates and ensures correct previous values. -
private→ no modifier onGetTargetPosition(iOS): Minor style change (removingprivatekeyword) is inconsequential in C#.
Selected Fix: PR's fix (over alternatives)
| Criterion | PR Fix | Best Alternative (Attempt 1) |
|---|---|---|
| Platforms fixed | ✅ Android + Windows + iOS/Mac | ❌ Android only |
| New fields added | 1 bool per platform | 0 new fields |
| Reviewer feedback addressed | ✅ try/finally + bounds check | N/A |
| Animation-driven cascades fixed | ✅ Yes (disables animation) | ✅ Yes (_gotoPosition blocks ScrollTo) |
| Maintainability | Clear, self-documenting flag | Requires understanding event subscribe/unsubscribe lifecycle |
kubaflo
left a comment
There was a problem hiding this comment.
Could you please resolve conflicts?
6e563dc to
00535e4
Compare
…ound in ItemsSource (#32141) <!-- 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! ### Issue Details - When a current item is set to a value that doesn't exist in the CarouselView's items source, the carousel incorrectly scrolls to the last item in a looped carousel. ### Root Cause CarouselViewHandler1 : - When CarouselView.CurrentItem is set to an item that is not present in the ItemsSource, ItemsSource.GetIndexForItem(invalidItem) returns -1, indicating that the item was not found. This -1 value is then passed through several methods: UpdateFromCurrentItem() calls ScrollToPosition(-1, currentPosition, animate), which triggers CarouselView.ScrollTo(-1, ...). In loop mode, this leads to CarouselViewHandler.ScrollToRequested being invoked with args.Index = -1. The handler then calls GetScrollToIndexPath(-1), which in turn invokes CarouselViewLoopManager.GetGoToIndex(collectionView, -1). Inside GetGoToIndex, arithmetic operations are performed on the invalid index, causing -1 to be treated as a valid position. As a result, the UICollectionView scrolls to this calculated physical position, which corresponds to the last logical item, producing unintended scroll behavior. CarouselViewHandler2 : - When CurrentItem is not found in ItemsSource, GetIndexForItem returns -1; in loop mode, CarouselViewLoopManager.GetCorrectedIndexPathFromIndex(-1) adds 1, incorrectly converting it to 0, which results in an unintended scroll to the last duplicated item. ### Description of Change - Added a check in ScrollToPosition methods in both CarouselViewController.cs and CarouselViewController2.cs to return early if goToPosition is less than zero, preventing unwanted scrolling when the target item is invalid. - **NOTE** : This [PR](#31275) resolves the issue of incorrect scrolling in loop mode when CurrentItem is not found in the ItemsSource, on Android. ### Issues Fixed Fixes #32139 ### Validated the behaviour in the following platforms - [x] Windows - [x] Android - [x] iOS - [x] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/48c77f1b-0819-4717-8cf6-68873f82ec1d"> | <video src="https://github.com/user-attachments/assets/1a667869-d79b-48fd-bc05-7ae3bd16a654"> |
🚦 Gate - Test Before and After Fix📊 Expand Full Gate —
|
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🖥️ Issue29529 Issue29529 |
✅ FAIL — 677s | ✅ PASS — 512s |
🔴 Without fix — 🖥️ Issue29529: FAIL ✅ · 677s
Determining projects to restore...
Restored /home/vsts/work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 568 ms).
Restored /home/vsts/work/1/s/src/Essentials/src/Essentials.csproj (in 4.5 sec).
Restored /home/vsts/work/1/s/src/Core/src/Core.csproj (in 5.96 sec).
Restored /home/vsts/work/1/s/src/Core/maps/src/Maps.csproj (in 2.4 sec).
Restored /home/vsts/work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj (in 37 ms).
Restored /home/vsts/work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 28 ms).
Restored /home/vsts/work/1/s/src/Controls/Maps/src/Controls.Maps.csproj (in 38 ms).
Restored /home/vsts/work/1/s/src/Controls/Foldable/src/Controls.Foldable.csproj (in 48 ms).
Restored /home/vsts/work/1/s/src/BlazorWebView/src/Maui/Microsoft.AspNetCore.Components.WebView.Maui.csproj (in 848 ms).
Restored /home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj (in 1.44 sec).
1 of 11 projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0-android36.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0-android36.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0-android36.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
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.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
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
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
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.60-ci+azdo.13736349
Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.dll
Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Foldable.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
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
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:08:33.51
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Determining projects to restore...
Restored /home/vsts/work/1/s/src/TestUtils/src/VisualTestUtils/VisualTestUtils.csproj (in 1.14 sec).
Restored /home/vsts/work/1/s/src/TestUtils/src/VisualTestUtils.MagickNet/VisualTestUtils.MagickNet.csproj (in 4.43 sec).
Restored /home/vsts/work/1/s/src/Controls/tests/TestCases.Android.Tests/Controls.TestCases.Android.Tests.csproj (in 5.77 sec).
Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.Core/UITest.Core.csproj (in 9 ms).
Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.Appium/UITest.Appium.csproj (in 2 ms).
Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.NUnit/UITest.NUnit.csproj (in 271 ms).
Restored /home/vsts/work/1/s/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj (in 5 ms).
Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.Analyzers/UITest.Analyzers.csproj (in 1.82 sec).
5 of 13 projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
Controls.CustomAttributes -> /home/vsts/work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
VisualTestUtils -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
UITest.Core -> /home/vsts/work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.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.NUnit -> /home/vsts/work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
UITest.Analyzers -> /home/vsts/work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
Controls.TestCases.Android.Tests -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.10] Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.30] 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
>>>>> 04/03/2026 14:32:11 FixtureSetup for Issue29529(Android)
>>>>> 04/03/2026 14:32:13 Issue29529VerifyPreviousPositionOnInsert Start
>>>>> 04/03/2026 14:32:18 Issue29529VerifyPreviousPositionOnInsert Stop
>>>>> 04/03/2026 14:32:18 Log types: logcat, bugreport, server
Failed Issue29529VerifyPreviousPositionOnInsert [5 s]
Error Message:
Assert.That(text, Is.EqualTo("Current Position: 0, Previous Position: 3"))
String lengths are both 41. Strings differ at index 40.
Expected: "Current Position: 0, Previous Position: 3"
But was: "Current Position: 0, Previous Position: 4"
---------------------------------------------------^
Stack Trace:
at Microsoft.Maui.TestCases.Tests.Issues.Issue29529.Issue29529VerifyPreviousPositionOnInsert() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29529.cs:line 22
1) at Microsoft.Maui.TestCases.Tests.Issues.Issue29529.Issue29529VerifyPreviousPositionOnInsert() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29529.cs:line 22
NUnit Adapter 4.5.0.0: Test execution complete
Total tests: 1
Failed: 1
Test Run Failed.
Total time: 27.4258 Seconds
🟢 With fix — 🖥️ Issue29529: PASS ✅ · 512s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0-android36.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0-android36.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0-android36.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
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.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Foldable.dll
Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-android36.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Xaml.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Maps.dll
Controls.TestCases.HostApp -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Controls.TestCases.HostApp.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Xaml.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.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Foldable.dll
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:06:36.49
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
Controls.CustomAttributes -> /home/vsts/work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13736349
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.
/home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.10] Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.29] 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
>>>>> 04/03/2026 14:40:46 FixtureSetup for Issue29529(Android)
>>>>> 04/03/2026 14:40:48 Issue29529VerifyPreviousPositionOnInsert Start
>>>>> 04/03/2026 14:40:52 Issue29529VerifyPreviousPositionOnInsert Stop
Passed Issue29529VerifyPreviousPositionOnInsert [4 s]
NUnit Adapter 4.5.0.0: Test execution complete
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 21.0709 Seconds
📁 Fix files reverted (4 files)
eng/pipelines/ci-copilot.ymlsrc/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cssrc/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cssrc/Controls/src/Core/Handlers/Items2/iOS/CarouselViewController2.cs
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
Windows
Position not updating on item add: CarouselView stayed at the old position after an item was added, leaving current/previous positions unsynced.
Cascading events: With
ItemsUpdatingScrollMode.KeepItemsInView, programmatic smooth scrolls triggered multiple ViewChanged calls, causingPositionChangedto fire repeatedly with intermediate values.Android
Programmatic smooth scrolls produced the same cascading
PositionChangedevents as on Windows.Description of Change
Windows
Position Update: On item add,
ItemsView.Positionis explicitly set based onItemsUpdatingScrollMode, keeping current and previous positions in sync.Prevent Cascading Events: Added
_isInternalPositionUpdate. For collection changes, animations are disabled (animate = false) so scrolling jumps directly, firingPositionChangedonly once.Android
Reused the
_isInternalPositionUpdatelogic. Disabled animations during collection changes, ensuring a single clean position update without duplicate events.Issues Fixed
Fixes #29529
Tested the behaviour in the following platforms
Screenshots