Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
46dbbf5
[Windows] Fix for 23293 ( 'Grouping for Vertical list without DataTem…
SyedAbdulAzeemSF4852 Mar 19, 2025
c566055
[Windows] Fix for 23293 ( 'Grouping for Vertical list without DataTem…
SyedAbdulAzeemSF4852 Mar 20, 2025
5673f1e
[Windows] Fix for 23293 ( 'Grouping for Vertical list without DataTem…
SyedAbdulAzeemSF4852 Mar 20, 2025
e400f3c
[Windows] Fix for 23293 ( 'Grouping for Vertical list without DataTem…
SyedAbdulAzeemSF4852 Mar 20, 2025
f35d4fc
[Windows] Fix for 23293 ( 'Grouping for Vertical list without DataTem…
SyedAbdulAzeemSF4852 Mar 20, 2025
ef1ba30
[Windows] Fix for 23293 ( 'Grouping for Vertical list without DataTem…
SyedAbdulAzeemSF4852 Mar 25, 2025
7c8e398
[Windows] Fix for 23293 (Grouping collection view without data templa…
SyedAbdulAzeemSF4852 Mar 25, 2025
8750e16
[Windows] Fix for 23293 (Grouping collection view without data templa…
SyedAbdulAzeemSF4852 Mar 27, 2025
09f05bd
[Windows] Fix for 23293 (Grouping collection view without data templa…
SyedAbdulAzeemSF4852 Apr 2, 2025
47808cc
[Windows] Fix for 23293 (Grouping collection view without data templa…
SyedAbdulAzeemSF4852 Apr 9, 2025
9a9bc0d
[Windows] Fix for 23293 (Grouping collection view without data templa…
SyedAbdulAzeemSF4852 Apr 15, 2025
7d89c8d
[Windows] Fix for 23293 (Grouping collection view without data templa…
SyedAbdulAzeemSF4852 Apr 21, 2025
b3cef0c
Have updated the test case and have added the iOS snapshot
SyedAbdulAzeemSF4852 Jun 9, 2025
de128d0
Fix Windows CollectionView grouped display bugs and update baseline s…
SyedAbdulAzeemSF4852 Apr 6, 2026
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Handlers;
using Microsoft.UI.Xaml.Controls;
Expand Down Expand Up @@ -41,6 +39,19 @@ protected override void UpdateItemTemplate()
{
base.UpdateItemTemplate();

// When a GroupFooterTemplate is set but no ItemTemplate, the fake footer item
// (GroupFooterItemTemplateContext) appended to each group's Items list needs a
// template to render correctly. Without a selector WinUI calls ToString() on it,
// displaying the class name instead of the bound footer content.
if (Element.ItemTemplate is null && ItemsView.IsGrouped && ItemsView.GroupFooterTemplate is not null)

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 Mapper and Property Patterns - The new ItemTemplateSelector state is only recalculated from UpdateItemTemplate(), but the condition also depends on IsGrouped and GroupFooterTemplate, whose mapper path calls UpdateItemsSource() instead. Concrete scenario: start grouped with no footer, then set GroupFooterTemplate at runtime; the grouped source is rebuilt with GroupFooterItemTemplateContext items, but the selector remains null so Windows still renders the footer context type name. The inverse also leaves a stale selector after clearing the footer or toggling grouping. Refresh this selector from the mapper paths for all dependent properties, or centralize the selector update in UpdateItemsSource()/a shared helper.

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 Mapper and Property Patterns — This selector is only updated from UpdateItemTemplate(), but GroupFooterTemplate changes are mapped through MapIsGrouped, which only calls UpdateItemsSource(). Concrete scenario: a grouped Windows CollectionView starts with ItemTemplate == null, then sets GroupFooterTemplate after the handler is connected; the fake footer item is added to the source, but ItemTemplateSelector remains null, so WinUI renders the GroupFooterItemTemplateContext type name instead of the footer template. Please update this selector from the grouped/header/footer mapper path as well.

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 Mapper and Property Patterns - The footer ItemTemplateSelector is only updated from UpdateItemTemplate(), but the condition also depends on IsGrouped and GroupFooterTemplate. On Windows those properties map through MapIsGrouped, which only rebuilds the items source, so dynamically setting GroupFooterTemplate or toggling IsGrouped after the handler is connected can create GroupFooterItemTemplateContext footer items while ListViewBase.ItemTemplateSelector remains null. Concrete scenario: create a grouped CollectionView without ItemTemplate, then assign GroupFooterTemplate later; the footer still renders as the context type name. Update this selector from the grouped/footer-template mapper path as well, or centralize it in a method called by both UpdateItemTemplate() and grouped-source updates.

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-Generated Review (multi-model)

⚠️ [moderate] Handler mapper lifecycle — This selector depends on GroupFooterTemplate, but the GroupFooterTemplate mapper only calls UpdateItemsSource() (MapIsGrouped) and does not re-run UpdateItemTemplate(). If an app sets or clears GroupFooterTemplate after the handler is connected, the items source is rebuilt with footer contexts but ItemTemplateSelector can remain stale, so footers still render as GroupFooterItemTemplateContext (or the selector remains enabled after clearing). Please update this selector from the grouped/header/footer template mapper path as well.

{
ListViewBase.ItemTemplateSelector = new GroupFooterDataTemplateSelector();

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 Mapper and Property Patterns The footer selector is only updated from UpdateItemTemplate(), but GroupFooterTemplateProperty maps to MapIsGrouped/UpdateItemsSource() in GroupableItemsViewHandler.cs. Concrete scenario: a grouped CollectionView starts with no footer template and later sets GroupFooterTemplate; the source is rebuilt with synthetic GroupFooterItemTemplateContext rows, but ItemTemplateSelector is never installed, so the footer can still render as the context type name. Please update the grouped template mapper path to refresh this selector when GroupFooterTemplate changes, including clearing it when the footer template is removed.

}
else
{
ListViewBase.ItemTemplateSelector = null;
}

ListViewBase.GroupStyleSelector = new GroupHeaderStyleSelector();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using UWPApp = Microsoft.UI.Xaml.Application;
using UWPDataTemplate = Microsoft.UI.Xaml.DataTemplate;
using WinDataTemplateSelector = Microsoft.UI.Xaml.Controls.DataTemplateSelector;

namespace Microsoft.Maui.Controls.Platform;

// Selects ItemsViewDefaultTemplate for fake group-footer items (GroupFooterItemTemplateContext)
// so that the GroupFooterTemplate is applied when no ItemTemplate is set on the CollectionView.
// All other items return null, letting WinUI fall back to its default ToString() rendering.
internal partial class GroupFooterDataTemplateSelector : WinDataTemplateSelector
{
readonly UWPDataTemplate? _footerTemplate;

public GroupFooterDataTemplateSelector()
{
_footerTemplate = UWPApp.Current.Resources["ItemsViewDefaultTemplate"] as UWPDataTemplate;
}

protected override UWPDataTemplate SelectTemplateCore(object item)
{
if (item is GroupFooterItemTemplateContext)
{
return _footerTemplate!;
}

return null!;
}

protected override UWPDataTemplate SelectTemplateCore(object item, Microsoft.UI.Xaml.DependencyObject container)
{
return SelectTemplateCore(item);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

Expand Down Expand Up @@ -67,13 +68,33 @@ public void Dispose()
GroupTemplateContext CreateGroupTemplateContext(object group)
{
var groupHeaderTemplateContext = _groupHeaderTemplate != null
? new ItemTemplateContext(_groupHeaderTemplate, group, _container, mauiContext: _mauiContext) : null;
? new ItemTemplateContext(_groupHeaderTemplate, group, _container, mauiContext: _mauiContext) : null;

var groupFooterTemplateContext = _groupFooterTemplate != null
? new GroupFooterItemTemplateContext(_groupFooterTemplate, group, _container, mauiContext: _mauiContext) : null;

// This is where we'll eventually look at GroupItemPropertyName
var groupItemsList = TemplatedItemSourceFactory.Create(group as IEnumerable, _itemTemplate, _container, mauiContext: _mauiContext);
object groupItemsList;
if (_itemTemplate is not null)
{
groupItemsList = TemplatedItemSourceFactory.Create(group as IEnumerable, _itemTemplate, _container, mauiContext: _mauiContext);
}
else
{
// When no ItemTemplate is set, copy the raw group items into a new list so that:

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 Shared Models - Copying grouped items into a new List<object> turns observable group contents into a one-time snapshot whenever ItemTemplate is null. Concrete scenario: a grouped CollectionView uses ObservableCollection<string> groups without an ItemTemplate; after the handler creates this raw list, adding/removing items inside a group raises INotifyCollectionChanged on the original group but the WinUI group item source never updates. Preserve the live observable/range-aware source behavior instead of raw-copying the group contents.

// WinUI calls ToString() on the actual data objects (not on ItemTemplateContext wrappers).
var rawItems = new List<object>();

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 Shared Models — Copying the group into a plain List<object> drops change notifications for observable groups when ItemTemplate is null. Previously, observable group lists flowed through ObservableItemTemplateCollection, which subscribed to the inner group's INotifyCollectionChanged; after this change, adding/removing items inside an ObservableCollection group will not update the Windows CollectionView. Please preserve a live/observable projection of the group items, especially for the no-footer case, or implement a wrapper that observes the source while still appending the synthetic footer without mutating user data.

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 Windows Copying no-template group items into a new List<object> drops live INotifyCollectionChanged behavior for observable groups. Previously TemplatedItemSourceFactory.Create(...) returned ObservableItemTemplateCollection for IList + INCC, so adding/removing items inside a group could update the WinUI grouped source. With this snapshot list, ObservableCollection<string> groups will not reflect later item changes. Please preserve/forward the original group collection notifications while still exposing raw items for no-template rendering.

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 Shared Models - Copying no-template group contents into a new List<object> disconnects the displayed group items from observable inner groups. If a group implements INotifyCollectionChanged, additions/removals inside that group will no longer update the Windows CollectionView because WinUI is bound to the one-time copy plus fake footer, not the original observable source or an observable wrapper. Preserve inner collection notifications for the no-ItemTemplate path, especially when appending the footer item.

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-Generated Review (multi-model)

[major] CollectionView Windows regression — Copying the group into a detached List<object> drops live updates from observable groups when ItemTemplate is null. Before this path, observable IList groups went through ObservableItemTemplateCollection and nested adds/removes were proxied into WinUI; after this copy, adding an item to an ObservableCollection group updates the source but not the rendered group. Please keep an observable wrapper/projection for raw items instead of snapshotting the group.

var groupEnumerable = group as IEnumerable;
if (groupEnumerable is not null)
{
foreach (var item in groupEnumerable)
{
rawItems.Add(item);
}
}

groupItemsList = rawItems;
}

return new GroupTemplateContext(groupHeaderTemplateContext, groupFooterTemplateContext, groupItemsList);
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue23293.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
namespace Controls.TestCases.HostApp.Issues;

[Issue(IssueTracker.Github, 23293, "Grouping collection view without data template results in displaying the default string representation of the object", PlatformAffected.UWP)]
public class Issue23293 : ContentPage
{
public List<Issue23293AnimalGroup> Animals { get; set; } = new List<Issue23293AnimalGroup>();

public Issue23293()
{
CreateAnimalsCollection();

CollectionView collectionViewWithoutDataTemplate = new CollectionView
{
AutomationId = "CollectionViewWithoutDataTemplate",
ItemsSource = Animals,
IsGrouped = true,
};

CollectionView collectionViewWithGroupTemplates = new CollectionView
{
AutomationId = "CollectionViewWithGroupTemplates",
ItemsSource = Animals,
IsGrouped = true,
GroupHeaderTemplate = new DataTemplate(() =>
{
Label label = new Label { FontSize = 14, FontAttributes = FontAttributes.Bold, BackgroundColor = Colors.LightGray };
label.SetBinding(Label.TextProperty, "Name");
return label;
}),
GroupFooterTemplate = new DataTemplate(() =>
{
Label label = new Label { FontSize = 12, BackgroundColor = Colors.LightBlue, AutomationId = "GroupFooterLabel" };
label.SetBinding(Label.TextProperty, "Name", stringFormat: "End of {0}");
return label;
}),
};

Grid grid = new Grid
{
Padding = 10,
};

grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });

grid.Add(new Label { Text = "No ItemTemplate - items should show animal names", FontSize = 12, FontAttributes = FontAttributes.Bold }, 0, 0);
grid.Add(collectionViewWithoutDataTemplate, 0, 1);
grid.Add(new Label { Text = "No ItemTemplate with Header/Footer - header shows group name, items show animal name, footer shows 'End of Bears'", FontSize = 12, FontAttributes = FontAttributes.Bold }, 0, 2);
grid.Add(collectionViewWithGroupTemplates, 0, 3);

Content = grid;
}

void CreateAnimalsCollection()
{
Animals.Add(new Issue23293AnimalGroup("Bears", new List<string>
{
"American Black Bear",
}));
}
}

public class Issue23293AnimalGroup : List<string>
{
public string Name { get; set; }

public Issue23293AnimalGroup(string name, List<string> animals) : base(animals)
{
Name = name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

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

public override string Issue => "Grouping collection view without data template results in displaying the default string representation of the object";

[Test]
[Category(UITestCategories.CollectionView)]
public void GroupedCollectionViewWithoutItemTemplateRendersCorrectly()
{
App.WaitForElement("GroupFooterLabel");
VerifyScreenshot();

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.

Pending snapshots already available in the latest build.

Example, iOS:
image

Could you commit the images?

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 , I have committed the snapshots.

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] Regression Prevention and Test Coverage - This issue is Windows-specific and the test calls VerifyScreenshot(), but the raw PR adds Android/iOS baselines only. On Windows the test either fails due to a missing baseline or does not validate the platform being fixed. Add the WinUI/Windows screenshot baseline for GroupedCollectionViewWithoutItemTemplateRendersCorrectly, or scope the screenshot verification so every platform where it runs has a baseline.

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] Regression Prevention This new test calls VerifyScreenshot() and is intended to validate the Windows fix, but the PR only adds Android/iOS baselines. The known Windows gate failure is therefore a missing snapshots/windows/GroupedCollectionViewWithoutItemTemplateRendersCorrectly.png, so the Windows regression test cannot pass or protect this fix until the Windows baseline is committed.

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] Regression Prevention and Test Coverage - This shared UI test calls VerifyScreenshot() for a UWP/Windows-specific issue, but the PR adds only Android and iOS baselines. On WinUI the screenshot lookup will fail because snapshots/windows/GroupedCollectionViewWithoutItemTemplateRendersCorrectly.png is missing, which matches the observed gate failure after GroupFooterLabel appears. Add the Windows baseline or make the screenshot assertion platform-scoped.

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-Generated Review (multi-model)

[major] Regression test coverage — This Windows-specific screenshot test calls VerifyScreenshot(), but the PR adds only Android/iOS baselines. On Windows the test reaches this line and fails because the baseline image is missing, so the gate cannot validate the fix. Please add the Windows snapshot baseline or make the test platform-gated to the baselines that exist.

}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading