ListView is deprecated in .NET 10. If you’re building new screens, use CollectionView. If you’re maintaining existing MAUI apps, you’ll need to migrate. The good news: most migrations are 90% mechanical find-and-replace. The 10% that isn’t mechanical — grouping, swipe actions, context menus — is covered in this guide.
This is a complete reference. Find your ListView pattern, see the CollectionView equivalent, migrate.
Table of Contents
- Why Migrate: CollectionView Advantages
- Basic List Migration
- Pull-to-Refresh
- Item Selection and Taps
- Context Actions → SwipeView
- Grouped Lists
- Empty State / No Data View
- Headers and Footers
- Infinite Scroll / Load More
- Grid Layout (No ListView Equivalent)
- Built-in Cell Types Migration
- Performance Tips
- FAQ
- Conclusion
Why Migrate: CollectionView Advantages
Before the patterns, here’s what you gain from migrating:
- No ViewCell overhead — DataTemplates contain views directly, reducing element count in the visual tree
- Flexible layouts — vertical, horizontal, or grid with
ItemsLayout - Built-in EmptyView — no wrapper hacks for empty state
- Better virtualization — more consistent recycling behavior across platforms
- Multiple selection — native API, no event handler workarounds
- Header/Footer as views — not separate list sections, simpler to customize
Basic List Migration
The simplest case: a list of items with a custom data template.
<!-- BEFORE: ListView -->
<ListView ItemsSource="{Binding Products}"
HasUnevenRows="True"
SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid Padding="16,12" ColumnDefinitions="Auto,*,Auto">
<Image Source="{Binding ImageUrl}" WidthRequest="48" HeightRequest="48" />
<VerticalStackLayout Grid.Column="1" Padding="12,0">
<Label Text="{Binding Name}" FontAttributes="Bold" />
<Label Text="{Binding Category}" TextColor="#666" />
</VerticalStackLayout>
<Label Grid.Column="2" Text="{Binding Price, StringFormat='${0:F2}'}"
TextColor="#4CAF50" FontAttributes="Bold" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- AFTER: CollectionView -->
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Product">
<!-- ViewCell removed — Grid goes directly in DataTemplate -->
<Grid Padding="16,12" ColumnDefinitions="Auto,*,Auto">
<Image Source="{Binding ImageUrl}" WidthRequest="48" HeightRequest="48" />
<VerticalStackLayout Grid.Column="1" Padding="12,0">
<Label Text="{Binding Name}" FontAttributes="Bold" />
<Label Text="{Binding Category}" TextColor="#666" />
</VerticalStackLayout>
<Label Grid.Column="2" Text="{Binding Price, StringFormat='${0:F2}'}"
TextColor="#4CAF50" FontAttributes="Bold" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Changes: Remove ViewCell, add x:DataType for compiled bindings, remove HasUnevenRows and SeparatorVisibility (CollectionView measures items automatically; add separators manually if needed).
Adding Separators (If Needed)
<CollectionView.ItemTemplate>
<DataTemplate>
<VerticalStackLayout>
<Grid Padding="16,12">
<!-- item content -->
</Grid>
<BoxView HeightRequest="1" BackgroundColor="#E0E0E0" Margin="16,0" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
Pull-to-Refresh
ListView had pull-to-refresh built in. CollectionView doesn’t — use a RefreshView wrapper instead.
<!-- BEFORE: ListView pull-to-refresh -->
<ListView ItemsSource="{Binding Items}"
IsPullToRefreshEnabled="True"
RefreshCommand="{Binding RefreshCommand}"
IsRefreshing="{Binding IsRefreshing}">
</ListView>
<!-- AFTER: RefreshView + CollectionView -->
<RefreshView Command="{Binding RefreshCommand}"
IsRefreshing="{Binding IsRefreshing}">
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate>
<!-- item template -->
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
The ViewModel code stays the same — RefreshCommand and IsRefreshing properties are unchanged.
Item Selection and Taps
Single Item Tap Navigation
<!-- BEFORE: ListView ItemSelected event -->
<ListView ItemSelected="OnItemSelected" />
// Code-behind
private async void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
{
if (e.SelectedItem is Product product)
{
((ListView)sender).SelectedItem = null; // Deselect
await Navigation.PushAsync(new ProductDetailPage(product));
}
}
<!-- AFTER: CollectionView with TapGestureRecognizer (MVVM-friendly) -->
<CollectionView ItemsSource="{Binding Products}" SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Product">
<Grid Padding="16,12">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type vm:ProductListViewModel}},
Path=NavigateToProductCommand}"
CommandParameter="{Binding .}" />
</Grid.GestureRecognizers>
<Label Text="{Binding Name}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
// ViewModel
[RelayCommand]
private async Task NavigateToProductAsync(Product product)
{
await Shell.Current.GoToAsync("productdetail",
new Dictionary<string, object> { { "Product", product } });
}
Single Selection Mode
<CollectionView SelectionMode="Single"
SelectedItem="{Binding SelectedProduct}"
SelectionChanged="OnSelectionChanged">
</CollectionView>
// Code-behind (if needed)
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selected = e.CurrentSelection.FirstOrDefault() as Product;
// e.PreviousSelection also available
}
Multiple Selection
<CollectionView SelectionMode="Multiple"
SelectedItems="{Binding SelectedProducts}">
</CollectionView>
// ViewModel
[ObservableProperty]
private IList<object> _selectedProducts = new List<object>();
Context Actions → SwipeView
ListView’s ContextActions (long-press menu on iOS, swipe on Android) become SwipeView in CollectionView.
<!-- BEFORE: ListView context actions -->
<ListView>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem Text="Delete"
IsDestructive="True"
Command="{Binding Source={x:Reference Page}, Path=BindingContext.DeleteCommand}"
CommandParameter="{Binding .}" />
<MenuItem Text="Archive"
Command="{Binding Source={x:Reference Page}, Path=BindingContext.ArchiveCommand}"
CommandParameter="{Binding .}" />
</ViewCell.ContextActions>
<Label Text="{Binding Title}" Padding="12" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- AFTER: CollectionView with SwipeView -->
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:TaskItem">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems Mode="Reveal">
<SwipeItem Text="Delete"
BackgroundColor="#F44336"
IconImageSource="delete.png"
Command="{Binding Source={RelativeSource AncestorType={x:Type vm:TaskViewModel}}, Path=DeleteCommand}"
CommandParameter="{Binding .}" />
<SwipeItem Text="Archive"
BackgroundColor="#9E9E9E"
IconImageSource="archive.png"
Command="{Binding Source={RelativeSource AncestorType={x:Type vm:TaskViewModel}}, Path=ArchiveCommand}"
CommandParameter="{Binding .}" />
</SwipeItems>
</SwipeView.RightItems>
<Grid Padding="16,12">
<Label Text="{Binding Title}" />
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
SwipeItems.Mode options: Reveal (slide to show actions, default) or Execute (swiping all the way triggers action immediately).
Grouped Lists
<!-- BEFORE: ListView grouping -->
<ListView ItemsSource="{Binding GroupedContacts}"
IsGroupingEnabled="True"
GroupDisplayBinding="{Binding Key}">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding Key}" BackgroundColor="#F5F5F5"
Padding="16,8" FontAttributes="Bold" />
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding Name}" Padding="16,12" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- AFTER: CollectionView grouping -->
<CollectionView ItemsSource="{Binding GroupedContacts}"
IsGrouped="True">
<CollectionView.GroupHeaderTemplate>
<DataTemplate x:DataType="model:ContactGroup">
<Label Text="{Binding Key}" BackgroundColor="#F5F5F5"
Padding="16,8" FontAttributes="Bold" />
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Contact">
<Label Text="{Binding Name}" Padding="16,12" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Group Data Structure
// ViewModel grouping setup
public ObservableCollection<ContactGroup> GroupedContacts { get; } = new();
// ContactGroup must implement IEnumerable and expose Key
public class ContactGroup : List<Contact>
{
public string Key { get; }
public ContactGroup(string key, List<Contact> contacts) : base(contacts)
{
Key = key;
}
}
// Build grouped collection
var grouped = contacts
.GroupBy(c => c.Name[0].ToString().ToUpper())
.OrderBy(g => g.Key)
.Select(g => new ContactGroup(g.Key, g.ToList()));
GroupedContacts = new ObservableCollection<ContactGroup>(grouped);
Empty State / No Data View
<!-- BEFORE: ListView empty state (hacky) -->
<ListView ItemsSource="{Binding Items}" />
<Label Text="No items yet"
IsVisible="{Binding Items.Count, Converter={StaticResource ZeroToBoolConverter}}" />
<!-- AFTER: CollectionView EmptyView (built-in) -->
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.EmptyView>
<VerticalStackLayout HorizontalOptions="Center" VerticalOptions="Center" Spacing="12">
<Image Source="empty_state.png" WidthRequest="120" />
<Label Text="No items yet" FontSize="18" HorizontalOptions="Center" />
<Label Text="Tap the + button to add your first item."
TextColor="#666" HorizontalOptions="Center" />
</VerticalStackLayout>
</CollectionView.EmptyView>
</CollectionView>
Headers and Footers
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.Header>
<Grid Padding="16" BackgroundColor="#6200EE">
<Label Text="Featured Products" TextColor="White" FontSize="20" FontAttributes="Bold" />
</Grid>
</CollectionView.Header>
<CollectionView.Footer>
<Button Text="Load More Products"
Command="{Binding LoadMoreCommand}"
IsVisible="{Binding HasMore}"
Margin="16" />
</CollectionView.Footer>
<CollectionView.ItemTemplate>
<DataTemplate><!-- item template --></DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Infinite Scroll / Load More
<CollectionView ItemsSource="{Binding Items}"
RemainingItemsThreshold="5"
RemainingItemsThresholdReached="OnThresholdReached"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}">
</CollectionView>
// ViewModel
[RelayCommand]
private async Task LoadMoreAsync()
{
if (IsLoadingMore || !HasMoreItems) return;
IsLoadingMore = true;
var nextPage = await _service.GetPageAsync(++_currentPage);
foreach (var item in nextPage)
Items.Add(item);
HasMoreItems = nextPage.Count == PageSize;
IsLoadingMore = false;
}
Grid Layout (No ListView Equivalent)
This is a CollectionView-only feature — ListView couldn’t do grids without third-party libraries:
<CollectionView ItemsSource="{Binding Photos}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical"
Span="3"
HorizontalItemSpacing="4"
VerticalItemSpacing="4" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Photo">
<Image Source="{Binding ThumbnailUrl}"
Aspect="AspectFill"
HeightRequest="120" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Built-in Cell Types Migration
ListView’s built-in cells (TextCell, ImageCell, SwitchCell, EntryCell) don’t have CollectionView equivalents. Build equivalent UI directly in the DataTemplate:
<!-- TextCell equivalent -->
<DataTemplate x:DataType="model:Item">
<Grid Padding="16,12" ColumnDefinitions="*,Auto">
<VerticalStackLayout>
<Label Text="{Binding Title}" FontAttributes="Bold" />
<Label Text="{Binding Detail}" TextColor="#666" FontSize="13" />
</VerticalStackLayout>
<Image Grid.Column="1" Source="chevron.png" WidthRequest="16"
VerticalOptions="Center" />
</Grid>
</DataTemplate>
<!-- ImageCell equivalent -->
<DataTemplate x:DataType="model:Item">
<Grid Padding="12" ColumnDefinitions="48,*">
<Image Source="{Binding ImageSource}" WidthRequest="40" HeightRequest="40"
Aspect="AspectFill" VerticalOptions="Center" />
<VerticalStackLayout Grid.Column="1" Padding="12,0" VerticalOptions="Center">
<Label Text="{Binding Title}" FontAttributes="Bold" />
<Label Text="{Binding Detail}" TextColor="#666" FontSize="13" />
</VerticalStackLayout>
</Grid>
</DataTemplate>
Performance Tips for CollectionView
- Always use
x:DataType— compiled bindings are measurably faster for lists, especially with 50+ items - Keep DataTemplates shallow — fewer layout layers = faster measurement and rendering
- Use
DataTemplateSelectorsparingly — multiple template types reduce recycling efficiency - Avoid transparency in cells — opaque backgrounds allow the renderer to skip compositing
- Pre-size images — avoid dynamic image resizing in cells by loading correctly-sized thumbnails
// DataTemplate selector for different item types
public class ItemTemplateSelector : DataTemplateSelector
{
public DataTemplate HeaderTemplate { get; set; } = default!;
public DataTemplate StandardTemplate { get; set; } = default!;
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
=> item is HeaderItem ? HeaderTemplate : StandardTemplate;
}
FAQ
Does CollectionView support the iOS native alphabet index scrubber?
Not out of the box. The native section index on iOS (the A-Z scrubber on the right side) requires a custom handler. Third-party libraries like DevExpress MAUI controls provide this if you need it.
My ListView had custom cell height. How do I set item height in CollectionView?
CollectionView measures each item automatically — you set the height through your DataTemplate layout (set HeightRequest on the root view if you need a fixed height). For uniform height lists, setting a fixed height improves performance since the layout engine skips measurement.
How do I programmatically scroll to a specific item in CollectionView?
// Scroll to item
myCollectionView.ScrollTo(item, animate: true);
// Scroll to index
myCollectionView.ScrollTo(index: 42, animate: true, position: ScrollToPosition.Start);
Can I use DataTemplateSelector with CollectionView grouping?
Yes — assign a DataTemplateSelector to GroupHeaderTemplate or ItemTemplate. Each item or group header is evaluated independently by the selector.
Is there a CollectionView equivalent of ListView’s RecycleElement strategy?
CollectionView handles recycling automatically and doesn’t expose a strategy option — it always uses the most efficient recycling approach for the platform. You don’t need to configure it.
Conclusion
Every ListView pattern has a clean CollectionView equivalent. The migration is mostly removing ViewCell, wrapping in RefreshView for pull-to-refresh, and replacing ContextActions with SwipeView. The grouping and selection APIs are structurally similar.
The time investment is worth it beyond avoiding future forced migration — CollectionView’s grid layout, EmptyView, and better virtualization are features you’ll actively benefit from. Migrate your highest-traffic screens first to maximize impact.
[INTERNAL_LINK: What’s new in .NET 10 for MAUI developers] [INTERNAL_LINK: .NET MAUI MVVM with CommunityToolkit.Mvvm guide]

Leave a Reply