From 9949e7c4428801c9916e7c4178aeaed4425d0c4b Mon Sep 17 00:00:00 2001 From: praveenkumarkarunanithi Date: Tue, 17 Mar 2026 10:13:38 +0530 Subject: [PATCH 1/6] fix and test update --- .../Items/Android/MauiCarouselRecyclerView.cs | 3 +- .../TestCases.HostApp/Issues/Issue29544.cs | 152 ++++++++++++++++++ .../Tests/Issues/Issue29544.cs | 84 ++++++++++ 3 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue29544.cs create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29544.cs diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index 2917b6060ed7..c1481049aa47 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -193,6 +193,7 @@ public override void ScrollTo(ScrollToRequestEventArgs args) if (args.IsAnimated) { + _gotoPosition = position; ScrollHelper.AnimateScrollToPosition(position, args.ScrollToPosition); } else @@ -506,8 +507,6 @@ void IMauiCarouselRecyclerView.UpdateFromCurrentItem() _gotoPosition = currentItemPosition; ItemsView.ScrollTo(currentItemPosition, position: Microsoft.Maui.Controls.ScrollToPosition.Center, animate: Carousel.AnimateCurrentItemChanges); } - - _gotoPosition = -1; } void IMauiCarouselRecyclerView.UpdateFromPosition() diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue29544.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue29544.cs new file mode 100644 index 000000000000..a5566eea288e --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue29544.cs @@ -0,0 +1,152 @@ +using System.Collections.ObjectModel; + +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 29544, + "PreviousItem and PreviousPosition not updating correctly on ScrollTo or Position set", + PlatformAffected.Android)] +public class Issue29544 : ContentPage +{ + readonly CarouselView _carouselView; + readonly ObservableCollection _items; + readonly Label _previousItemLabel; + readonly Label _previousPositionLabel; + readonly Label _currentItemLabel; + readonly Label _currentPositionLabel; + + public Issue29544() + { + _items = new ObservableCollection + { + "Item 1", + "Item 2", + "Item 3", + "Item 4", + "Item 5" + }; + + _currentItemLabel = new Label + { + AutomationId = "CurrentItemLabel", + Text = "Current Item: Item 1" + }; + + _previousItemLabel = new Label + { + AutomationId = "PreviousItemLabel", + Text = "Previous Item: none" + }; + + _currentPositionLabel = new Label + { + AutomationId = "CurrentPositionLabel", + Text = "Current Position: 0" + }; + + _previousPositionLabel = new Label + { + AutomationId = "PreviousPositionLabel", + Text = "Previous Position: none" + }; + + _carouselView = new CarouselView + { + AutomationId = "CarouselView", + Loop = false, + ItemsSource = _items, + ItemTemplate = new DataTemplate(() => + { + var label = new Label + { + FontSize = 24, + BackgroundColor = Colors.LightGray, + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Center, + HorizontalTextAlignment = TextAlignment.Center + }; + label.SetBinding(Label.TextProperty, "."); + return label; + }) + }; + + _carouselView.CurrentItemChanged += OnCurrentItemChanged; + _carouselView.PositionChanged += OnPositionChanged; + + var scrollTo3Button = new Button + { + AutomationId = "ScrollTo3Button", + Text = "Scroll To 3" + }; + scrollTo3Button.Clicked += (s, e) => _carouselView.ScrollTo(3); + + var scrollTo1Button = new Button + { + AutomationId = "ScrollTo1Button", + Text = "Scroll To 1" + }; + scrollTo1Button.Clicked += (s, e) => _carouselView.ScrollTo(1); + + var setPosition3Button = new Button + { + AutomationId = "SetPosition3Button", + Text = "Set Position 3" + }; + setPosition3Button.Clicked += (s, e) => _carouselView.Position = 3; + + var setPosition0Button = new Button + { + AutomationId = "SetPosition0Button", + Text = "Set Position 0" + }; + setPosition0Button.Clicked += (s, e) => _carouselView.Position = 0; + + Content = new Grid + { + Padding = new Thickness(10), + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star } + }, + Children = + { + new VerticalStackLayout + { + Spacing = 6, + Children = + { + _currentItemLabel, + _previousItemLabel, + _currentPositionLabel, + _previousPositionLabel, + new HorizontalStackLayout + { + Spacing = 8, + Children = { scrollTo3Button, scrollTo1Button } + }, + new HorizontalStackLayout + { + Spacing = 8, + Children = { setPosition3Button, setPosition0Button } + } + } + }, + _carouselView + } + }; + + Grid.SetRow(_carouselView, 1); + } + + void OnCurrentItemChanged(object sender, CurrentItemChangedEventArgs e) + { + _currentItemLabel.Text = $"Current Item: {e.CurrentItem}"; + _previousItemLabel.Text = $"Previous Item: {e.PreviousItem ?? "none"}"; + } + + void OnPositionChanged(object sender, PositionChangedEventArgs e) + { + _currentPositionLabel.Text = $"Current Position: {e.CurrentPosition}"; + _previousPositionLabel.Text = $"Previous Position: {e.PreviousPosition}"; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29544.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29544.cs new file mode 100644 index 000000000000..d6ec9ff75e4f --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29544.cs @@ -0,0 +1,84 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue29544 : _IssuesUITest +{ + public override string Issue => + "PreviousItem and PreviousPosition not updating correctly on ScrollTo or Position set"; + + public Issue29544(TestDevice device) : base(device) { } + + [Test] + [Category(UITestCategories.CarouselView)] + public void PreviousPositionUpdatesCorrectlyOnScrollTo() + { + App.WaitForElement("ScrollTo3Button"); + App.Tap("ScrollTo3Button"); + + App.RetryAssert(() => + { + var currentPos = App.FindElement("CurrentPositionLabel").GetText(); + Assert.That(currentPos, Does.Contain("3"), + "CarouselView should reach position 3 after ScrollTo"); + }); + + var previousPositionText = App.FindElement("PreviousPositionLabel").GetText(); + Assert.That(previousPositionText, Does.Contain("0"), + "After scrolling from position 0 to 3, PreviousPosition should be 0"); + + var previousItemText = App.FindElement("PreviousItemLabel").GetText(); + Assert.That(previousItemText, Does.Contain("Item 1"), + "After scrolling from position 0 to 3, PreviousItem should be 'Item 1'"); + + App.Tap("ScrollTo1Button"); + + App.RetryAssert(() => + { + var currentPos = App.FindElement("CurrentPositionLabel").GetText(); + Assert.That(currentPos, Does.Contain("1"), + "CarouselView should reach position 1 after second ScrollTo"); + }); + + var secondPreviousPosition = App.FindElement("PreviousPositionLabel").GetText(); + Assert.That(secondPreviousPosition, Does.Contain("3"), + "After scrolling from position 3 to 1, PreviousPosition should be 3"); + + var secondPreviousItem = App.FindElement("PreviousItemLabel").GetText(); + Assert.That(secondPreviousItem, Does.Contain("Item 4"), + "After scrolling from position 3 to 1, PreviousItem should be 'Item 4'"); + } + + [Test] + [Category(UITestCategories.CarouselView)] + public void PreviousPositionUpdatesCorrectlyOnSetPosition() + { + App.WaitForElement("SetPosition0Button"); + App.Tap("SetPosition0Button"); + + App.RetryAssert(() => + { + var pos = App.FindElement("CurrentPositionLabel").GetText(); + Assert.That(pos, Does.Contain("0"), "CarouselView should be at position 0"); + }); + + App.Tap("SetPosition3Button"); + + App.RetryAssert(() => + { + var currentPos = App.FindElement("CurrentPositionLabel").GetText(); + Assert.That(currentPos, Does.Contain("3"), + "CarouselView should reach position 3 after setting Position = 3"); + }); + + var previousPositionText = App.FindElement("PreviousPositionLabel").GetText(); + Assert.That(previousPositionText, Does.Contain("0"), + "After setting Position from 0 to 3, PreviousPosition should be 0"); + + var previousItemText = App.FindElement("PreviousItemLabel").GetText(); + Assert.That(previousItemText, Does.Contain("Item 1"), + "After setting Position from 0 to 3, PreviousItem should be 'Item 1'"); + } +} From 7b278bb58ef277e43d62554dacc56f47ea212911 Mon Sep 17 00:00:00 2001 From: praveenkumarkarunanithi Date: Tue, 17 Mar 2026 23:55:10 +0530 Subject: [PATCH 2/6] updating test and fix for windows --- .../Items/CarouselViewHandler.Windows.cs | 43 +++++++++++++++++++ .../net-windows/PublicAPI.Unshipped.txt | 1 + .../TestCases.HostApp/Issues/Issue29544.cs | 2 +- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs index 5efafef7e98b..ff3692460557 100644 --- a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Specialized; using System.Linq; +using System.Threading.Tasks; using Microsoft.Maui.Controls.Platform; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -25,6 +26,7 @@ public partial class CarouselViewHandler : ItemsViewHandler WScrollBarVisibility? _verticalScrollBarVisibilityWithoutLoop; Size _currentSize; bool _isCarouselViewReady; + int _gotoPosition = -1; NotifyCollectionChangedEventHandler _collectionChanged; readonly WeakNotifyCollectionChangedProxy _proxy = new(); @@ -190,6 +192,24 @@ public static void MapCurrentItem(CarouselViewHandler handler, CarouselView caro handler.UpdateCurrentItem(); } + protected override async Task ScrollTo(ScrollToRequestEventArgs args) + { + if (args.IsAnimated && args.Mode == ScrollToMode.Position) + { + _gotoPosition = args.Index; + + // Commit Position immediately so PositionChanged fires with the correct + // PreviousPosition/PreviousItem before the animation starts. The visual scroll follows + // asynchronously. This mirrors the Android fix and ensures the label updates + // without waiting for the WinUI animation to settle (which can stall in test environments). + SetCarouselViewPosition(_gotoPosition); + } + + await base.ScrollTo(args); + + _gotoPosition = -1; + } + public static void MapPosition(CarouselViewHandler handler, CarouselView carouselView) { // If the initial position hasn't been set, we have a UpdateInitialPosition call on CarouselViewHandler @@ -385,7 +405,14 @@ void UpdateCurrentItem() var currentItemPosition = GetItemPositionInCarousel(ItemsView.CurrentItem); if (currentItemPosition < 0 || currentItemPosition >= ItemCount) + { return; + } + + if (_gotoPosition != -1) + { + return; + } ItemsView.ScrollTo(currentItemPosition, position: ScrollToPosition.Center, animate: ItemsView.AnimateCurrentItemChanges); } @@ -491,6 +518,15 @@ void UpdateScrollBarVisibilityForLoop() void CarouselScrolled(object sender, ItemsViewScrolledEventArgs e) { + + // Ignore scroll events that fire before the initial position is established. + // On Windows, WinUI can fire ViewChanged during initial layout with an incorrect + // center index, which would incorrectly override the intended initial position. + if (!InitialPositionSet) + { + return; + } + var position = e.CenterItemIndex; if (position == -1) @@ -503,6 +539,13 @@ void CarouselScrolled(object sender, ItemsViewScrolledEventArgs e) return; } + // Suppress all events during a programmatic animated scroll. + // Position is committed immediately in ScrollTo before the animation starts. + if (_gotoPosition != -1) + { + return; + } + SetCarouselViewPosition(position); } diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 173b902095ea..6c5783cbe09c 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ #nullable enable +~override Microsoft.Maui.Controls.Handlers.Items.CarouselViewHandler.ScrollTo(Microsoft.Maui.Controls.ScrollToRequestEventArgs args) -> System.Threading.Tasks.Task override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue29544.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue29544.cs index a5566eea288e..3d15f7b02916 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issue29544.cs +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue29544.cs @@ -4,7 +4,7 @@ namespace Maui.Controls.Sample.Issues; [Issue(IssueTracker.Github, 29544, "PreviousItem and PreviousPosition not updating correctly on ScrollTo or Position set", - PlatformAffected.Android)] + PlatformAffected.Android | PlatformAffected.UWP)] public class Issue29544 : ContentPage { readonly CarouselView _carouselView; From 374db596b9d39c0f8d741b0fdc21ba59944ee626 Mon Sep 17 00:00:00 2001 From: praveenkumarkarunanithi Date: Wed, 18 Mar 2026 11:52:17 +0530 Subject: [PATCH 3/6] Update CarouselViewHandler.Windows.cs --- .../Handlers/Items/CarouselViewHandler.Windows.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs index ff3692460557..11bbe6845b70 100644 --- a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs @@ -205,9 +205,17 @@ protected override async Task ScrollTo(ScrollToRequestEventArgs args) SetCarouselViewPosition(_gotoPosition); } - await base.ScrollTo(args); - - _gotoPosition = -1; + try + { + await base.ScrollTo(args); + } + finally + { + // Only reset if this call still owns _gotoPosition — a concurrent animated + // ScrollTo may have already replaced it with a different target. + if (_gotoPosition == args.Index) + _gotoPosition = -1; + } } public static void MapPosition(CarouselViewHandler handler, CarouselView carouselView) @@ -518,7 +526,6 @@ void UpdateScrollBarVisibilityForLoop() void CarouselScrolled(object sender, ItemsViewScrolledEventArgs e) { - // Ignore scroll events that fire before the initial position is established. // On Windows, WinUI can fire ViewChanged during initial layout with an incorrect // center index, which would incorrectly override the intended initial position. From 964fea5e8f274f55663b7d1b7cf20a0f65473f66 Mon Sep 17 00:00:00 2001 From: praveenkumarkarunanithi Date: Wed, 18 Mar 2026 16:03:16 +0530 Subject: [PATCH 4/6] Update CarouselViewHandler.Windows.cs --- .../Handlers/Items/CarouselViewHandler.Windows.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs index 11bbe6845b70..c278cb8fd6ca 100644 --- a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs @@ -198,10 +198,7 @@ protected override async Task ScrollTo(ScrollToRequestEventArgs args) { _gotoPosition = args.Index; - // Commit Position immediately so PositionChanged fires with the correct - // PreviousPosition/PreviousItem before the animation starts. The visual scroll follows - // asynchronously. This mirrors the Android fix and ensures the label updates - // without waiting for the WinUI animation to settle (which can stall in test environments). + // Commit position before animation so PreviousPosition/PreviousItem are correct immediately. SetCarouselViewPosition(_gotoPosition); } @@ -211,8 +208,7 @@ protected override async Task ScrollTo(ScrollToRequestEventArgs args) } finally { - // Only reset if this call still owns _gotoPosition — a concurrent animated - // ScrollTo may have already replaced it with a different target. + // Conditional reset guards against a concurrent ScrollTo replacing the target. if (_gotoPosition == args.Index) _gotoPosition = -1; } @@ -526,9 +522,7 @@ void UpdateScrollBarVisibilityForLoop() void CarouselScrolled(object sender, ItemsViewScrolledEventArgs e) { - // Ignore scroll events that fire before the initial position is established. - // On Windows, WinUI can fire ViewChanged during initial layout with an incorrect - // center index, which would incorrectly override the intended initial position. + // Ignore ViewChanged events fired before the initial position is established. if (!InitialPositionSet) { return; @@ -546,8 +540,7 @@ void CarouselScrolled(object sender, ItemsViewScrolledEventArgs e) return; } - // Suppress all events during a programmatic animated scroll. - // Position is committed immediately in ScrollTo before the animation starts. + // Suppress intermediate scroll events during a programmatic animated scroll. if (_gotoPosition != -1) { return; From 0cd6fdc504d385d4045dcaf63a436954ff0b1771 Mon Sep 17 00:00:00 2001 From: praveenkumarkarunanithi Date: Thu, 19 Mar 2026 18:31:25 +0530 Subject: [PATCH 5/6] Update MauiCarouselRecyclerView.cs --- .../src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index c1481049aa47..3cb72dbfc1c9 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -193,7 +193,7 @@ public override void ScrollTo(ScrollToRequestEventArgs args) if (args.IsAnimated) { - _gotoPosition = position; + _gotoPosition = args.Index; ScrollHelper.AnimateScrollToPosition(position, args.ScrollToPosition); } else From e6bb4bf60d43bf035076c75babcbb1931487afd9 Mon Sep 17 00:00:00 2001 From: praveenkumarkarunanithi Date: Tue, 24 Mar 2026 13:42:24 +0530 Subject: [PATCH 6/6] Update MauiCarouselRecyclerView.cs --- .../Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index 3cb72dbfc1c9..16d4ce4f6c9e 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -185,6 +185,7 @@ public override void ScrollTo(ScrollToRequestEventArgs args) // And at the same time the user is requesting we go to a particular item if (position == -1) { + _gotoPosition = -1; if (Carousel.Loop) _carouselViewLoopManager.AddPendingScrollTo(args); @@ -193,7 +194,8 @@ public override void ScrollTo(ScrollToRequestEventArgs args) if (args.IsAnimated) { - _gotoPosition = args.Index; + if (_gotoPosition == -1) + _gotoPosition = args.Index; ScrollHelper.AnimateScrollToPosition(position, args.ScrollToPosition); } else