Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ public override void Scrolled(UIScrollView scrollView)
{
var (visibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex) = GetVisibleItemsIndex();

if (!visibleItems)
if (!visibleItems && !HasHeaderOrFooter())
{
return;
}

var contentInset = scrollView.ContentInset;
var contentOffsetX = scrollView.ContentOffset.X + contentInset.Left;
Expand Down Expand Up @@ -147,6 +149,37 @@ protected virtual (bool VisibleItems, int First, int Center, int Last) GetVisibl
return (VisibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex);
}

bool HasHeaderOrFooter()
Copy link

Copilot AI Jul 2, 2025

Choose a reason for hiding this comment

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

This helper method is duplicated in ItemsViewDelegator2; consider extracting it into a shared base class or utility to avoid code duplication.

Copilot uses AI. Check for mistakes.
{
var viewController = ViewController;

if (viewController?.ItemsView is null)
{
return false;
}

// Check for structured headers/footers
if (viewController.ItemsView is StructuredItemsView structuredItemsView)
{
if (structuredItemsView.Header is not null || structuredItemsView.HeaderTemplate is not null ||
structuredItemsView.Footer is not null || structuredItemsView.FooterTemplate is not null)
{
return true;
}
}

// Check for group headers/footers
if (viewController.ItemsView is GroupableItemsView groupableItemsView && groupableItemsView.IsGrouped)
{
if (groupableItemsView.GroupHeaderTemplate is not null || groupableItemsView.GroupFooterTemplate is not null)
{
return true;
}
}

return false;
}

static int GetItemIndex(NSIndexPath indexPath, IItemsViewSource itemSource)
{
int index = (int)indexPath.Item;
Expand Down
35 changes: 34 additions & 1 deletion src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ public override void Scrolled(UIScrollView scrollView)
{
var (visibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex) = GetVisibleItemsIndex();

if (!visibleItems)
if (!visibleItems && !HasHeaderOrFooter())
{
return;
}

var contentInset = scrollView.ContentInset;
var contentOffsetX = scrollView.ContentOffset.X + contentInset.Left;
Expand Down Expand Up @@ -148,6 +150,37 @@ protected virtual (bool VisibleItems, int First, int Center, int Last) GetVisibl
return (VisibleItems, firstVisibleItemIndex, centerItemIndex, lastVisibleItemIndex);
}

bool HasHeaderOrFooter()
Copy link

Copilot AI Jul 2, 2025

Choose a reason for hiding this comment

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

Duplicate logic from the other delegator; extracting this into a common helper would improve maintainability.

Copilot uses AI. Check for mistakes.
{
var viewController = ViewController;

if (viewController?.ItemsView is null)
{
return false;
}

// Check for structured headers/footers (overall CollectionView header/footer)
if (viewController.ItemsView is StructuredItemsView structuredItemsView)
{
if (structuredItemsView.Header is not null || structuredItemsView.HeaderTemplate is not null ||
structuredItemsView.Footer is not null || structuredItemsView.FooterTemplate is not null)
{
return true;
}
}

// Check for group headers/footers (grouped CollectionView)
if (viewController.ItemsView is GroupableItemsView groupableItemsView && groupableItemsView.IsGrouped)
{
if (groupableItemsView.GroupHeaderTemplate is not null || groupableItemsView.GroupFooterTemplate is not null)
{
return true;
}
}

return false;
}

static int GetItemIndex(NSIndexPath indexPath, IItemsViewSource itemSource)
{
int index = (int)indexPath.Item;
Expand Down
83 changes: 83 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue30249.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Collections.ObjectModel;

namespace Controls.TestCases.HostApp.Issues;

[Issue(IssueTracker.Github, 30249, "Grouped CollectionView does not trigger the Scrolled event for empty groups", PlatformAffected.iOS)]
public class Issue30249 : ContentPage
{
CollectionView collectionViewWithEmptyGroups;
ObservableCollection<Issue30249Group> groupedItems = new ObservableCollection<Issue30249Group>();
Label scrolledEventStatusLabel;

public Issue30249()
{
Label descriptionLabel = new Label
{
Text = "Verify CollectionView Scrolled event is triggered for empty groups",
Margin = new Thickness(10)
};

scrolledEventStatusLabel = new Label
{
AutomationId = "ScrolledEventStatusLabel",
Copy link

Copilot AI Jul 2, 2025

Choose a reason for hiding this comment

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

Ensure these AutomationIds are unique across the host app to prevent conflicts when calling WaitForElement in shared tests.

Copilot uses AI. Check for mistakes.
Text = "Failure",
Margin = new Thickness(10)
};

collectionViewWithEmptyGroups = new CollectionView
{
AutomationId = "CollectionViewWithEmptyGroups",
IsGrouped = true
};
collectionViewWithEmptyGroups.Scrolled += CollectionView_Scrolled;

collectionViewWithEmptyGroups.GroupHeaderTemplate = new DataTemplate(() =>
{
Label label = new Label
{
Padding = new Thickness(10),
};

label.SetBinding(Label.TextProperty, "Title");
return label;
});

for (int group = 0; group < 20; group++)
{
groupedItems.Add(new Issue30249Group($"Group {group}", new List<string>()));
}

collectionViewWithEmptyGroups.ItemsSource = groupedItems;

Grid grid = new Grid
{
RowDefinitions =
{
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Star }
}
};

grid.Add(descriptionLabel, 0, 0);
grid.Add(scrolledEventStatusLabel, 0, 1);
grid.Add(collectionViewWithEmptyGroups, 0, 2);

Content = grid;
}

private void CollectionView_Scrolled(object sender, ItemsViewScrolledEventArgs e)
{
scrolledEventStatusLabel.Text = "Success";
}
}

public class Issue30249Group : List<string>
{
public string Title { get; set; }

public Issue30249Group(string title, List<string> items) : base(items)
{
Title = title;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue30249 : _IssuesUITest
{
public Issue30249(TestDevice device) : base(device)
{
}

public override string Issue => "Grouped CollectionView does not trigger the Scrolled event for empty groups";

[Test]
[Category(UITestCategories.CollectionView)]
public void VerifyScrolledEventForEmptyGroups()
{
App.WaitForElement("CollectionViewWithEmptyGroups");
App.ScrollDown("CollectionViewWithEmptyGroups", ScrollStrategy.Gesture, 0.2, 500);

Copy link

Copilot AI Jul 2, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider using App.WaitForElement("ScrolledEventStatusLabel") before accessing its text to reduce test flakiness on slower devices or CI environments.

Suggested change
App.WaitForElement("ScrolledEventStatusLabel");

Copilot uses AI. Check for mistakes.
var scrolledEventStatus = App.FindElement("ScrolledEventStatusLabel").GetText();
Assert.That(scrolledEventStatus, Is.EqualTo("Success"));
}
}