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). |
Addressed all AI Agent concerns — corrected |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Conflicts have been resolved in the branch. |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
4ab9e00 to
0bd4d56
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"> |
…ound in ItemsSource (dotnet#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](dotnet#31275) resolves the issue of incorrect scrolling in loop mode when CurrentItem is not found in the ItemsSource, on Android. ### Issues Fixed Fixes dotnet#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"> |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…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"> |
… on collection update (#31275) <!-- 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! ### 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, causing `PositionChanged` to fire repeatedly with intermediate values. **Android** Programmatic smooth scrolls produced the same cascading `PositionChanged` events as on Windows. ### Description of Change **Windows** Position Update: On item add, `ItemsView.Position` is explicitly set based on `ItemsUpdatingScrollMode`, keeping current and previous positions in sync. Prevent Cascading Events: Added `_isInternalPositionUpdate`. For collection changes, animations are disabled (animate = false) so scrolling jumps directly, firing `PositionChanged` only once. **Android** Reused the `_isInternalPositionUpdate` logic. 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Screenshots | Before Issue Fix | After Issue Fix | |------------------|-----------------| | <img width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/d66f0352-a91f-4b85-bb9f-e0e54e55aa5f" /> | <img width="350" alt="withfix" src="https://github.com/user-attachments/assets/d120f268-6954-498d-aab0-42bc3745e296" /> | ---------
…ound in ItemsSource (dotnet#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](dotnet#31275) resolves the issue of incorrect scrolling in loop mode when CurrentItem is not found in the ItemsSource, on Android. ### Issues Fixed Fixes dotnet#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"> |
… on collection update (dotnet#31275) <!-- 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! ### 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, causing `PositionChanged` to fire repeatedly with intermediate values. **Android** Programmatic smooth scrolls produced the same cascading `PositionChanged` events as on Windows. ### Description of Change **Windows** Position Update: On item add, `ItemsView.Position` is explicitly set based on `ItemsUpdatingScrollMode`, keeping current and previous positions in sync. Prevent Cascading Events: Added `_isInternalPositionUpdate`. For collection changes, animations are disabled (animate = false) so scrolling jumps directly, firing `PositionChanged` only once. **Android** Reused the `_isInternalPositionUpdate` logic. Disabled animations during collection changes, ensuring a single clean position update without duplicate events. ### Issues Fixed Fixes dotnet#29529 Tested the behaviour in the following platforms - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Screenshots | Before Issue Fix | After Issue Fix | |------------------|-----------------| | <img width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/d66f0352-a91f-4b85-bb9f-e0e54e55aa5f" /> | <img width="350" alt="withfix" src="https://github.com/user-attachments/assets/d120f268-6954-498d-aab0-42bc3745e296" /> | ---------
…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"> |
…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"> |
… on collection update (#31275) <!-- 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! ### 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, causing `PositionChanged` to fire repeatedly with intermediate values. **Android** Programmatic smooth scrolls produced the same cascading `PositionChanged` events as on Windows. ### Description of Change **Windows** Position Update: On item add, `ItemsView.Position` is explicitly set based on `ItemsUpdatingScrollMode`, keeping current and previous positions in sync. Prevent Cascading Events: Added `_isInternalPositionUpdate`. For collection changes, animations are disabled (animate = false) so scrolling jumps directly, firing `PositionChanged` only once. **Android** Reused the `_isInternalPositionUpdate` logic. 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Screenshots | Before Issue Fix | After Issue Fix | |------------------|-----------------| | <img width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/d66f0352-a91f-4b85-bb9f-e0e54e55aa5f" /> | <img width="350" alt="withfix" src="https://github.com/user-attachments/assets/d120f268-6954-498d-aab0-42bc3745e296" /> | ---------
…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"> |
… on collection update (#31275) <!-- 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! ### 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, causing `PositionChanged` to fire repeatedly with intermediate values. **Android** Programmatic smooth scrolls produced the same cascading `PositionChanged` events as on Windows. ### Description of Change **Windows** Position Update: On item add, `ItemsView.Position` is explicitly set based on `ItemsUpdatingScrollMode`, keeping current and previous positions in sync. Prevent Cascading Events: Added `_isInternalPositionUpdate`. For collection changes, animations are disabled (animate = false) so scrolling jumps directly, firing `PositionChanged` only once. **Android** Reused the `_isInternalPositionUpdate` logic. 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Screenshots | Before Issue Fix | After Issue Fix | |------------------|-----------------| | <img width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/d66f0352-a91f-4b85-bb9f-e0e54e55aa5f" /> | <img width="350" alt="withfix" src="https://github.com/user-attachments/assets/d120f268-6954-498d-aab0-42bc3745e296" /> | ---------
… on collection update (#31275) <!-- 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! ### 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, causing `PositionChanged` to fire repeatedly with intermediate values. **Android** Programmatic smooth scrolls produced the same cascading `PositionChanged` events as on Windows. ### Description of Change **Windows** Position Update: On item add, `ItemsView.Position` is explicitly set based on `ItemsUpdatingScrollMode`, keeping current and previous positions in sync. Prevent Cascading Events: Added `_isInternalPositionUpdate`. For collection changes, animations are disabled (animate = false) so scrolling jumps directly, firing `PositionChanged` only once. **Android** Reused the `_isInternalPositionUpdate` logic. 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Screenshots | Before Issue Fix | After Issue Fix | |------------------|-----------------| | <img width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/d66f0352-a91f-4b85-bb9f-e0e54e55aa5f" /> | <img width="350" alt="withfix" src="https://github.com/user-attachments/assets/d120f268-6954-498d-aab0-42bc3745e296" /> | ---------
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