Maui Collectionview
.NET (Microsoft)OfficialPublished by .NET (Microsoft)
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
CollectionView is the primary .NET MAUI control for displaying scrollable lists and grids of data. This skill provides essential patterns for data binding, layout configuration, selection, grouping, and performance optimization, while emphasizing best practices like compiled bindings and proper threading.
What it does
- Configures list and grid layouts for scrollable data collections.
- Implements item selection, grouping, and pull-to-refresh functionality.
- Supports incremental loading for infinite scrolling scenarios.
- Provides guidance on using DataTemplates with compiled bindings.
- Offers performance optimization techniques like ItemSizingStrategy.
- Defines how to handle empty states and custom headers or footers.
When to use it
- When displaying a scrollable list or grid of data items.
- When you need to bind a collection of objects to a templated item layout.
- When implementing features like selection, grouping, or pull-to-refresh.
- When you need to optimize list performance for large datasets.
- When replacing legacy ListView controls in .NET MAUI.
Advantages
- Superior performance compared to older list controls.
- Flexible layout options including vertical and horizontal lists or grids.
- No requirement for ViewCell, allowing for simpler template structures.
- Built-in support for advanced features like swipe actions and incremental loading.
Usage examples
- “How do I create a simple vertical list in .NET MAUI using CollectionView?”
- Provides a XAML snippet showing a CollectionView bound to an ObservableCollection with a basic DataTemplate.
- “How can I implement a grid layout in my MAUI app?”
- Shows how to configure the ItemsLayout property using GridItemsLayout with specific span and spacing settings.
- “How do I show a message when my list is empty?”
- Demonstrates the use of the EmptyView property to display a string or a custom view when the data source is empty.
- “How do I improve scroll performance for a long list?”
- Explains how to set the ItemSizingStrategy to MeasureFirstItem on the CollectionView for uniform item sizes.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/dotnet/maui-collectionview/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip maui-collectionview.zip -d .agents/skills
Documentation by .NET (Microsoft)
CollectionView — .NET MAUI
CollectionView is the primary control for displaying scrollable lists and grids of data in .NET MAUI. It replaces ListView with better performance, flexible layouts, and no ViewCell requirement.
When to Use
Displaying a scrollable list or grid of data items
Binding a collection of objects to a templated item layout
Adding selection (single or multiple), grouping, or pull-to-refresh
Implementing infinite scroll / incremental loading
Showing swipe actions on list items
Displaying an empty state when no data is available
When Not to Use
Static layouts with a fixed number of items — use
GridorStackLayoutdirectlyMap pin lists — use the
Microsoft.Maui.Controls.MapsNuGet packageTable-based data entry forms — use standard form controls
Simple text-only lists with no interaction — consider
BindableLayouton aStackLayout
Scope Control — Answer Only What Was Asked
This skill is a reference you consult, not a checklist you apply. Most requests need one or two sections from it. Pulling in the rest makes the answer worse.
Stop conditions — do NOT act when:
The user asked a narrow question. Answer that question only. Do not append grouping, swipe actions, empty views, snap points, or performance tips that were not asked about.
The user's existing code already works. Do not rewrite working markup to match the examples here. Point out a concrete defect; if there is none, say so and answer the question that was asked.
The change is stylistic. Renaming, reordering attributes, or restructuring a template that already behaves correctly is churn, not a fix.
The control isn't
CollectionView.CarouselView,BindableLayout, andListView-in-maintenance code have different rules. Do not rewriteListViewcode the user did not ask about — but if they ask which control to use, or are migrating from Xamarin.Forms, recommendCollectionView(see Migrating from ListView).The problem is really a binding, DI, or navigation problem that happens to involve a list — defer to
maui-data-binding,maui-dependency-injection, ormaui-shell-navigation.
The API sections below are a reference, not a checklist — offer them only when relevant. Four rules are non-negotiable, because violating them produces code that does not work or silently loses compile-time checking:
Never use
ViewCellas aDataTemplateroot inCollectionView.Use
ObservableCollection<T>when the list mutates after first render.Mutate the bound collection on the UI thread.
Set
x:DataTypeon everyDataTemplate(and on the page root) for compiled bindings.
Everything else — sizing strategy, snap points, header/footer, empty views — is optional and should be offered only when it addresses the user's actual problem.
Inputs
A data source (typically
ObservableCollection<T>) bound toItemsSourceA
DataTemplatedefining how each item rendersOptional: layout configuration, selection mode, grouping model, empty view
Basic Setup
A complete, copy-pasteable page. Two things are load-bearing: the xmlns:models
declaration that every x:DataType="models:Item" in this skill assumes, and the
root x:DataType — without it the outer ItemsSource binding is not compiled:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:MyApp.Models"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:DataType="vm:ItemsViewModel"
x:Class="MyApp.ItemsPage">
<ContentPage.BindingContext>
<vm:ItemsViewModel />
</ContentPage.BindingContext>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<HorizontalStackLayout Padding="8" Spacing="8">
<Image Source="{Binding Icon}" WidthRequest="40" HeightRequest="40" />
<Label Text="{Binding Name}" VerticalOptions="Center" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>Later snippets show only the CollectionView element. When you hand a snippet to a
user, include the matching xmlns: declaration for any prefix it uses, or the XAML
will not compile.
The inline <ContentPage.BindingContext> above keeps the example self-contained. In
an app that uses dependency injection, register the ViewModel instead and assign it
through constructor injection (BindingContext = vm;) — see the
maui-dependency-injection skill.
Key rules:
Bind
ItemsSourceto anObservableCollection<T>so the UI updates on add/remove.Each item template root must be a
LayoutorView— never useViewCell.Always set
x:DataTypeonDataTemplatefor compiled bindings.
Layouts
Set ItemsLayout to control arrangement. Default is VerticalList.
| Layout | XAML value |
|---|---|
| Vertical list | VerticalList (default) |
| Horizontal list | HorizontalList |
| Vertical grid | GridItemsLayout with Orientation="Vertical" |
| Horizontal grid | GridItemsLayout with Orientation="Horizontal" |
Grid Layout
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical"
Span="2"
VerticalItemSpacing="8"
HorizontalItemSpacing="8" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<Border Padding="8" StrokeThickness="0">
<VerticalStackLayout>
<Image Source="{Binding Image}" HeightRequest="120" Aspect="AspectFill" />
<Label Text="{Binding Name}" FontAttributes="Bold" />
</VerticalStackLayout>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>Horizontal List
<CollectionView ItemsSource="{Binding Items}"
ItemsLayout="HorizontalList" />Selection
Selection Mode
| Mode | Property to bind | Binding mode |
|---|---|---|
None | — | — |
Single | SelectedItem | TwoWay |
Multiple | SelectedItems | OneWay |
<CollectionView ItemsSource="{Binding Items}"
SelectionMode="Single"
SelectedItem="{Binding CurrentItem, Mode=TwoWay}"
SelectionChangedCommand="{Binding ItemSelectedCommand}" />For Multiple selection, bind SelectedItems (type IList<object>):
<CollectionView SelectionMode="Multiple"
SelectedItems="{Binding ChosenItems, Mode=OneWay}" />Selected Visual State
Highlight selected items using VisualStateManager:
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<Grid Padding="8">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent" />
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Label Text="{Binding Name}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>Grouping
Create a group class inheriting from
List<T>:
public class AnimalGroup : List<Animal>
{
public string Name { get; }
public AnimalGroup(string name, List<Animal> animals) : base(animals)
{
Name = name;
}
}Bind to
ObservableCollection<AnimalGroup>and setIsGrouped="True":
<CollectionView ItemsSource="{Binding AnimalGroups}"
IsGrouped="True">
<CollectionView.GroupHeaderTemplate>
<DataTemplate x:DataType="models:AnimalGroup">
<Label Text="{Binding Name}"
FontAttributes="Bold"
BackgroundColor="{StaticResource Gray100}"
Padding="8" />
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Animal">
<Label Text="{Binding Name}" Padding="16,4" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>Pull-to-Refresh
Wrap CollectionView in a RefreshView. Set IsRefreshing back to false when done:
<RefreshView IsRefreshing="{Binding IsRefreshing}"
Command="{Binding RefreshCommand}">
<CollectionView ItemsSource="{Binding Items}" />
</RefreshView>Incremental Loading (Infinite Scroll)
<CollectionView ItemsSource="{Binding Items}"
RemainingItemsThreshold="5"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}" />⚠️ Do NOT use with non-virtualizing layouts.
LinearItemsLayoutandGridItemsLayoutsupport virtualization. UsingBindableLayouton aStackLayoutas an alternative toCollectionViewhas no virtualization, which triggers infinite threshold-reached events.
SwipeView — Binding from Inside DataTemplate
Commands inside a DataTemplate can't directly reach your ViewModel. Use RelativeSource AncestorType:
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="Delete"
BackgroundColor="Red"
Command="{Binding BindingContext.DeleteCommand, Source={RelativeSource AncestorType={x:Type ContentPage}}}"
CommandParameter="{Binding}" />
</SwipeItems>
</SwipeView.RightItems>
<Grid Padding="8">
<Label Text="{Binding Name}" />
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>EmptyView
Shown when ItemsSource is empty or null.
<CollectionView ItemsSource="{Binding SearchResults}"
EmptyView="No items found." />For a custom empty view, wrap in ContentView:
<CollectionView ItemsSource="{Binding SearchResults}">
<CollectionView.EmptyView>
<ContentView>
<VerticalStackLayout HorizontalOptions="Center" VerticalOptions="Center">
<Image Source="empty_state.png" WidthRequest="120" />
<Label Text="Nothing here yet" HorizontalTextAlignment="Center" />
</VerticalStackLayout>
</ContentView>
</CollectionView.EmptyView>
</CollectionView>Headers and Footers
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.Header>
<Label Text="Header" FontAttributes="Bold" Padding="8" />
</CollectionView.Header>
<CollectionView.Footer>
<Label Text="Footer" FontAttributes="Italic" Padding="8" />
</CollectionView.Footer>
</CollectionView>Use HeaderTemplate / FooterTemplate when headers or footers are data-bound.
Scrolling
ScrollTo
Programmatically scroll by index or item:
// Scroll to index
collectionView.ScrollTo(index: 10, position: ScrollToPosition.Center, animate: true);
// Scroll to item
collectionView.ScrollTo(item: myItem, position: ScrollToPosition.MakeVisible, animate: true);| ScrollToPosition | Behavior |
|---|---|
MakeVisible | Scrolls just enough to make the item visible |
Start | Scrolls item to the start of the viewport |
Center | Scrolls item to the center of the viewport |
End | Scrolls item to the end of the viewport |
Snap Points
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"
SnapPointsType="MandatorySingle"
SnapPointsAlignment="Center" />
</CollectionView.ItemsLayout>SnapPointsType:None,Mandatory,MandatorySingleSnapPointsAlignment:Start,Center,End
Migrating from ListView
ListView still compiles, but as of .NET 10 it is marked [Obsolete]
("ListView is deprecated. Please use CollectionView instead."). It is not
obsolete on .NET 9 and earlier, so check the project's target framework before
describing it as deprecated. If the user asks
which control to use, or is migrating from Xamarin.Forms, recommend
CollectionView — it is faster, needs no ViewCell, and supports flexible
layouts. What to avoid is silently rewriting ListView code the user did not ask
you to touch.
ListView | CollectionView equivalent |
|---|---|
ViewCell template root | Any View/Layout root — ViewCell is not supported |
ItemSelected event | SelectionChanged event, or SelectionChangedCommand |
ItemTapped event | A TapGestureRecognizer in the item template — SelectionChanged only fires when the selection changes, so it will not re-fire on tapping the already-selected item |
IsPullToRefreshEnabled + Refreshing | Wrap the CollectionView in a RefreshView |
IsGroupingEnabled | IsGrouped |
HasUnevenRows="True" | Default ItemSizingStrategy="MeasureAllItems" |
RowHeight (fixed height) | Set the height in the item template. MeasureFirstItem only reuses the first item's measured size — it is not an explicit row height |
SeparatorVisibility / SeparatorColor | No equivalent — draw a BoxView/Border in the item template |
The missing separator API is the most common migration surprise: CollectionView
has no built-in separators, so add one to the template yourself.
Performance Tips
Apply these only when the user reports a performance problem or explicitly asks about performance — they are not a default checklist.
Use
MeasureFirstItemfor uniform item sizes — significantly faster than the defaultMeasureAllItems, which measures every item individually. Set it on theCollectionViewitself (it is declared onStructuredItemsView), not onLinearItemsLayout/GridItemsLayout:<CollectionView ItemsSource="{Binding Items}" ItemSizingStrategy="MeasureFirstItem"> <CollectionView.ItemTemplate> <DataTemplate x:DataType="models:Item"> <Grid Padding="8" ColumnDefinitions="44,*" ColumnSpacing="8"> <Image WidthRequest="44" HeightRequest="44" /> <Label Grid.Column="1" Text="{Binding Name}" VerticalOptions="Center" /> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView>When
MeasureFirstItemis the wrong choice — keep the defaultMeasureAllItemsif:Items vary in height (wrapping text, optional rows, images of differing aspect) — the first item's size is applied to all, so the rest are clipped or stretched.
A
DataTemplateSelectorreturns different templates — the first item won't represent the others.The first item is atypical (a header-like or "featured" row) — every item inherits its size. Fixing this by reordering data is a smell; use
MeasureAllItemsinstead.Item size depends on runtime data that isn't loaded yet when the first item is measured.
Use
ObservableCollection<T>when the list mutates after first render. It implementsINotifyCollectionChanged, so in-placeAdd/Remove/Insertupdate the UI incrementally. AList<T>is fine for a list that never changes after it is bound. Note that replacingItemsSourcere-renders everything regardless of the collection type — so mutate the bound collection in place rather than reassigning it.Update collections on the UI thread —
MainThread.BeginInvokeOnMainThread(() => Items.Add(item)).
Common Pitfalls
| Issue | Fix |
|---|---|
| UI doesn't update when items change | Use ObservableCollection<T>, not List<T>. |
| App crashes or blank items | Never use ViewCell — use Grid, StackLayout, or any View as template root. |
| Items disappear or layout breaks | Always update ItemsSource and the collection on the UI thread (MainThread.BeginInvokeOnMainThread). |
| Incremental loading fires endlessly | Don't use StackLayout as layout; use LinearItemsLayout or GridItemsLayout. |
| EmptyView doesn't render correctly | Wrap custom empty views in ContentView. |
| Poor scroll performance | Use MeasureFirstItem sizing strategy for uniform item sizes. |
ItemSizingStrategy doesn't compile | It is declared on StructuredItemsView — set it on <CollectionView>, not on <LinearItemsLayout> / <GridItemsLayout>. |
| Items clipped or stretched | MeasureFirstItem assumes uniform item size. Use the default MeasureAllItems for variable-height items. |
| Selected state not visible | Add VisualState Name="Selected" to the item template root element. |
| Binding errors in SwipeView commands | Use RelativeSource AncestorType to reach the ViewModel from inside the item template. |
Validation
Before returning CollectionView markup you wrote or edited, confirm:
[ ] The
DataTemplateroot is aView/Layout— notViewCell.[ ]
DataTemplatedeclaresx:DataTypefor compiled bindings.[ ]
ItemsSourceis bound toObservableCollection<T>if the list mutates.[ ]
ItemSizingStrategy(if used) is on<CollectionView>, not on the layout.[ ]
Multipleselection bindsSelectedItems;SinglebindsSelectedItem(TwoWay).[ ]
RefreshView.IsRefreshingis set back tofalsewhen the refresh completes.[ ] The answer covers only what the user asked — no unrequested sections.
References
Files
- ATTRIBUTION.mdAdded by AutoGineer849 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md19.8 KiB
- references/collectionview-api.md7.2 KiB
SKILL.md
CollectionView — .NET MAUI
CollectionView is the primary control for displaying scrollable lists and grids of data in .NET MAUI. It replaces ListView with better performance, flexible layouts, and no ViewCell requirement.
When to Use
Displaying a scrollable list or grid of data items
Binding a collection of objects to a templated item layout
Adding selection (single or multiple), grouping, or pull-to-refresh
Implementing infinite scroll / incremental loading
Showing swipe actions on list items
Displaying an empty state when no data is available
When Not to Use
Static layouts with a fixed number of items — use
GridorStackLayoutdirectlyMap pin lists — use the
Microsoft.Maui.Controls.MapsNuGet packageTable-based data entry forms — use standard form controls
Simple text-only lists with no interaction — consider
BindableLayouton aStackLayout
Scope Control — Answer Only What Was Asked
This skill is a reference you consult, not a checklist you apply. Most requests need one or two sections from it. Pulling in the rest makes the answer worse.
Stop conditions — do NOT act when:
The user asked a narrow question. Answer that question only. Do not append grouping, swipe actions, empty views, snap points, or performance tips that were not asked about.
The user's existing code already works. Do not rewrite working markup to match the examples here. Point out a concrete defect; if there is none, say so and answer the question that was asked.
The change is stylistic. Renaming, reordering attributes, or restructuring a template that already behaves correctly is churn, not a fix.
The control isn't
CollectionView.CarouselView,BindableLayout, andListView-in-maintenance code have different rules. Do not rewriteListViewcode the user did not ask about — but if they ask which control to use, or are migrating from Xamarin.Forms, recommendCollectionView(see Migrating from ListView).The problem is really a binding, DI, or navigation problem that happens to involve a list — defer to
maui-data-binding,maui-dependency-injection, ormaui-shell-navigation.
The API sections below are a reference, not a checklist — offer them only when relevant. Four rules are non-negotiable, because violating them produces code that does not work or silently loses compile-time checking:
Never use
ViewCellas aDataTemplateroot inCollectionView.Use
ObservableCollection<T>when the list mutates after first render.Mutate the bound collection on the UI thread.
Set
x:DataTypeon everyDataTemplate(and on the page root) for compiled bindings.
Everything else — sizing strategy, snap points, header/footer, empty views — is optional and should be offered only when it addresses the user's actual problem.
Inputs
A data source (typically
ObservableCollection<T>) bound toItemsSourceA
DataTemplatedefining how each item rendersOptional: layout configuration, selection mode, grouping model, empty view
Basic Setup
A complete, copy-pasteable page. Two things are load-bearing: the xmlns:models
declaration that every x:DataType="models:Item" in this skill assumes, and the
root x:DataType — without it the outer ItemsSource binding is not compiled:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:MyApp.Models"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:DataType="vm:ItemsViewModel"
x:Class="MyApp.ItemsPage">
<ContentPage.BindingContext>
<vm:ItemsViewModel />
</ContentPage.BindingContext>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<HorizontalStackLayout Padding="8" Spacing="8">
<Image Source="{Binding Icon}" WidthRequest="40" HeightRequest="40" />
<Label Text="{Binding Name}" VerticalOptions="Center" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>Later snippets show only the CollectionView element. When you hand a snippet to a
user, include the matching xmlns: declaration for any prefix it uses, or the XAML
will not compile.
The inline <ContentPage.BindingContext> above keeps the example self-contained. In
an app that uses dependency injection, register the ViewModel instead and assign it
through constructor injection (BindingContext = vm;) — see the
maui-dependency-injection skill.
Key rules:
Bind
ItemsSourceto anObservableCollection<T>so the UI updates on add/remove.Each item template root must be a
LayoutorView— never useViewCell.Always set
x:DataTypeonDataTemplatefor compiled bindings.
Layouts
Set ItemsLayout to control arrangement. Default is VerticalList.
| Layout | XAML value |
|---|---|
| Vertical list | VerticalList (default) |
| Horizontal list | HorizontalList |
| Vertical grid | GridItemsLayout with Orientation="Vertical" |
| Horizontal grid | GridItemsLayout with Orientation="Horizontal" |
Grid Layout
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical"
Span="2"
VerticalItemSpacing="8"
HorizontalItemSpacing="8" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<Border Padding="8" StrokeThickness="0">
<VerticalStackLayout>
<Image Source="{Binding Image}" HeightRequest="120" Aspect="AspectFill" />
<Label Text="{Binding Name}" FontAttributes="Bold" />
</VerticalStackLayout>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>Horizontal List
<CollectionView ItemsSource="{Binding Items}"
ItemsLayout="HorizontalList" />Selection
Selection Mode
| Mode | Property to bind | Binding mode |
|---|---|---|
None | — | — |
Single | SelectedItem | TwoWay |
Multiple | SelectedItems | OneWay |
<CollectionView ItemsSource="{Binding Items}"
SelectionMode="Single"
SelectedItem="{Binding CurrentItem, Mode=TwoWay}"
SelectionChangedCommand="{Binding ItemSelectedCommand}" />For Multiple selection, bind SelectedItems (type IList<object>):
<CollectionView SelectionMode="Multiple"
SelectedItems="{Binding ChosenItems, Mode=OneWay}" />Selected Visual State
Highlight selected items using VisualStateManager:
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<Grid Padding="8">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent" />
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Label Text="{Binding Name}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>Grouping
Create a group class inheriting from
List<T>:
public class AnimalGroup : List<Animal>
{
public string Name { get; }
public AnimalGroup(string name, List<Animal> animals) : base(animals)
{
Name = name;
}
}Bind to
ObservableCollection<AnimalGroup>and setIsGrouped="True":
<CollectionView ItemsSource="{Binding AnimalGroups}"
IsGrouped="True">
<CollectionView.GroupHeaderTemplate>
<DataTemplate x:DataType="models:AnimalGroup">
<Label Text="{Binding Name}"
FontAttributes="Bold"
BackgroundColor="{StaticResource Gray100}"
Padding="8" />
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Animal">
<Label Text="{Binding Name}" Padding="16,4" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>Pull-to-Refresh
Wrap CollectionView in a RefreshView. Set IsRefreshing back to false when done:
<RefreshView IsRefreshing="{Binding IsRefreshing}"
Command="{Binding RefreshCommand}">
<CollectionView ItemsSource="{Binding Items}" />
</RefreshView>Incremental Loading (Infinite Scroll)
<CollectionView ItemsSource="{Binding Items}"
RemainingItemsThreshold="5"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}" />⚠️ Do NOT use with non-virtualizing layouts.
LinearItemsLayoutandGridItemsLayoutsupport virtualization. UsingBindableLayouton aStackLayoutas an alternative toCollectionViewhas no virtualization, which triggers infinite threshold-reached events.
SwipeView — Binding from Inside DataTemplate
Commands inside a DataTemplate can't directly reach your ViewModel. Use RelativeSource AncestorType:
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="Delete"
BackgroundColor="Red"
Command="{Binding BindingContext.DeleteCommand, Source={RelativeSource AncestorType={x:Type ContentPage}}}"
CommandParameter="{Binding}" />
</SwipeItems>
</SwipeView.RightItems>
<Grid Padding="8">
<Label Text="{Binding Name}" />
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>EmptyView
Shown when ItemsSource is empty or null.
<CollectionView ItemsSource="{Binding SearchResults}"
EmptyView="No items found." />For a custom empty view, wrap in ContentView:
<CollectionView ItemsSource="{Binding SearchResults}">
<CollectionView.EmptyView>
<ContentView>
<VerticalStackLayout HorizontalOptions="Center" VerticalOptions="Center">
<Image Source="empty_state.png" WidthRequest="120" />
<Label Text="Nothing here yet" HorizontalTextAlignment="Center" />
</VerticalStackLayout>
</ContentView>
</CollectionView.EmptyView>
</CollectionView>Headers and Footers
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.Header>
<Label Text="Header" FontAttributes="Bold" Padding="8" />
</CollectionView.Header>
<CollectionView.Footer>
<Label Text="Footer" FontAttributes="Italic" Padding="8" />
</CollectionView.Footer>
</CollectionView>Use HeaderTemplate / FooterTemplate when headers or footers are data-bound.
Scrolling
ScrollTo
Programmatically scroll by index or item:
// Scroll to index
collectionView.ScrollTo(index: 10, position: ScrollToPosition.Center, animate: true);
// Scroll to item
collectionView.ScrollTo(item: myItem, position: ScrollToPosition.MakeVisible, animate: true);| ScrollToPosition | Behavior |
|---|---|
MakeVisible | Scrolls just enough to make the item visible |
Start | Scrolls item to the start of the viewport |
Center | Scrolls item to the center of the viewport |
End | Scrolls item to the end of the viewport |
Snap Points
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"
SnapPointsType="MandatorySingle"
SnapPointsAlignment="Center" />
</CollectionView.ItemsLayout>SnapPointsType:None,Mandatory,MandatorySingleSnapPointsAlignment:Start,Center,End
Migrating from ListView
ListView still compiles, but as of .NET 10 it is marked [Obsolete]
("ListView is deprecated. Please use CollectionView instead."). It is not
obsolete on .NET 9 and earlier, so check the project's target framework before
describing it as deprecated. If the user asks
which control to use, or is migrating from Xamarin.Forms, recommend
CollectionView — it is faster, needs no ViewCell, and supports flexible
layouts. What to avoid is silently rewriting ListView code the user did not ask
you to touch.
ListView | CollectionView equivalent |
|---|---|
ViewCell template root | Any View/Layout root — ViewCell is not supported |
ItemSelected event | SelectionChanged event, or SelectionChangedCommand |
ItemTapped event | A TapGestureRecognizer in the item template — SelectionChanged only fires when the selection changes, so it will not re-fire on tapping the already-selected item |
IsPullToRefreshEnabled + Refreshing | Wrap the CollectionView in a RefreshView |
IsGroupingEnabled | IsGrouped |
HasUnevenRows="True" | Default ItemSizingStrategy="MeasureAllItems" |
RowHeight (fixed height) | Set the height in the item template. MeasureFirstItem only reuses the first item's measured size — it is not an explicit row height |
SeparatorVisibility / SeparatorColor | No equivalent — draw a BoxView/Border in the item template |
The missing separator API is the most common migration surprise: CollectionView
has no built-in separators, so add one to the template yourself.
Performance Tips
Apply these only when the user reports a performance problem or explicitly asks about performance — they are not a default checklist.
Use
MeasureFirstItemfor uniform item sizes — significantly faster than the defaultMeasureAllItems, which measures every item individually. Set it on theCollectionViewitself (it is declared onStructuredItemsView), not onLinearItemsLayout/GridItemsLayout:<CollectionView ItemsSource="{Binding Items}" ItemSizingStrategy="MeasureFirstItem"> <CollectionView.ItemTemplate> <DataTemplate x:DataType="models:Item"> <Grid Padding="8" ColumnDefinitions="44,*" ColumnSpacing="8"> <Image WidthRequest="44" HeightRequest="44" /> <Label Grid.Column="1" Text="{Binding Name}" VerticalOptions="Center" /> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView>When
MeasureFirstItemis the wrong choice — keep the defaultMeasureAllItemsif:Items vary in height (wrapping text, optional rows, images of differing aspect) — the first item's size is applied to all, so the rest are clipped or stretched.
A
DataTemplateSelectorreturns different templates — the first item won't represent the others.The first item is atypical (a header-like or "featured" row) — every item inherits its size. Fixing this by reordering data is a smell; use
MeasureAllItemsinstead.Item size depends on runtime data that isn't loaded yet when the first item is measured.
Use
ObservableCollection<T>when the list mutates after first render. It implementsINotifyCollectionChanged, so in-placeAdd/Remove/Insertupdate the UI incrementally. AList<T>is fine for a list that never changes after it is bound. Note that replacingItemsSourcere-renders everything regardless of the collection type — so mutate the bound collection in place rather than reassigning it.Update collections on the UI thread —
MainThread.BeginInvokeOnMainThread(() => Items.Add(item)).
Common Pitfalls
| Issue | Fix |
|---|---|
| UI doesn't update when items change | Use ObservableCollection<T>, not List<T>. |
| App crashes or blank items | Never use ViewCell — use Grid, StackLayout, or any View as template root. |
| Items disappear or layout breaks | Always update ItemsSource and the collection on the UI thread (MainThread.BeginInvokeOnMainThread). |
| Incremental loading fires endlessly | Don't use StackLayout as layout; use LinearItemsLayout or GridItemsLayout. |
| EmptyView doesn't render correctly | Wrap custom empty views in ContentView. |
| Poor scroll performance | Use MeasureFirstItem sizing strategy for uniform item sizes. |
ItemSizingStrategy doesn't compile | It is declared on StructuredItemsView — set it on <CollectionView>, not on <LinearItemsLayout> / <GridItemsLayout>. |
| Items clipped or stretched | MeasureFirstItem assumes uniform item size. Use the default MeasureAllItems for variable-height items. |
| Selected state not visible | Add VisualState Name="Selected" to the item template root element. |
| Binding errors in SwipeView commands | Use RelativeSource AncestorType to reach the ViewModel from inside the item template. |
Validation
Before returning CollectionView markup you wrote or edited, confirm:
[ ] The
DataTemplateroot is aView/Layout— notViewCell.[ ]
DataTemplatedeclaresx:DataTypefor compiled bindings.[ ]
ItemsSourceis bound toObservableCollection<T>if the list mutates.[ ]
ItemSizingStrategy(if used) is on<CollectionView>, not on the layout.[ ]
Multipleselection bindsSelectedItems;SinglebindsSelectedItem(TwoWay).[ ]
RefreshView.IsRefreshingis set back tofalsewhen the refresh completes.[ ] The answer covers only what the user asked — no unrequested sections.
References
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- .NET (Microsoft)
- Publisher
- .NET (Microsoft) (Official)
- Source
- dotnet/skills · plugins/dotnet-maui/skills/maui-collectionview · commit 96c3d68
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by .NET (Microsoft) or .NET (Microsoft).
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download maui-collectionview.zip