TheBetterListView.MAUI
1.0.1
dotnet add package TheBetterListView.MAUI --version 1.0.1
NuGet\Install-Package TheBetterListView.MAUI -Version 1.0.1
<PackageReference Include="TheBetterListView.MAUI" Version="1.0.1" />
<PackageVersion Include="TheBetterListView.MAUI" Version="1.0.1" />
<PackageReference Include="TheBetterListView.MAUI" />
paket add TheBetterListView.MAUI --version 1.0.1
#r "nuget: TheBetterListView.MAUI, 1.0.1"
#:package TheBetterListView.MAUI@1.0.1
#addin nuget:?package=TheBetterListView.MAUI&version=1.0.1
#tool nuget:?package=TheBetterListView.MAUI&version=1.0.1
TheBetterListView.MAUI
A virtualized, recycling items view for .NET MAUI with deterministic cell sizing — grid and list layouts from one control.
Why
MAUI's CollectionView on iOS wraps UICollectionView and relies on UIKit's self-sizing cell dequeue. During recycle, MAUI can measure a cell before its async content (e.g. an AspectFit image) has decoded, feeding a collapsed height to the flow layout — the cell renders as a thin horizontal strip. Pinning an exact HeightRequest on the template masks the collapse but makes GridItemsLayout drift and overlap rows. It is an implementation bug, not a tuning problem.
BetterItemsView owns its layout math instead:
- No collapse, by construction — the cell frame height is always exactly
ItemHeight; it never depends on measuring item content. - No overlap, by construction — every cell position is computed arithmetically from its index (
row = index / ColumnCount); two indices can never produce intersecting rects. - True virtualization — only visible cells (+
BufferRowsof overscan) are realized; views are pooled and recycled by rebindingBindingContext. Realized count stays constant regardless of item count. - No layout passes while scrolling — recycled cells are positioned with
TranslationX/Y, which doesn't invalidate layout. - Pure shared code — a
ScrollViewover anAbsoluteLayoutcanvas; no per-platform handlers, so iOS, Android, MacCatalyst, and Windows all run the same implementation.
Install
dotnet add package TheBetterListView.MAUI
No UseXxx() builder registration is needed — there are no custom handlers.
API
| Member | Type | Default | Notes |
|---|---|---|---|
ItemsSource |
IEnumerable (bindable) |
null | Observes INotifyCollectionChanged when implemented. Reassigning the whole source resets scroll to top (matches CollectionView). |
ItemTemplate |
DataTemplate (bindable) |
null | Must create a View (not a Cell). Rebinding on recycle re-evaluates DataTriggers, so model-driven selection templates work unchanged. |
ColumnCount |
int (bindable) |
1 | 1 = list layout. Supports {OnIdiom ...}. Avoid changing it at runtime on rotation — fixed spans keep rotation smooth. |
ItemHeight |
double (bindable) |
100 | The exact, always-honored cell height. Choose it from your template's known structure. |
RowSpacing |
double (bindable) |
0 | Vertical gap between rows. |
ColumnSpacing |
double (bindable) |
0 | Horizontal gap between columns; cell width = (viewportWidth − gaps) / ColumnCount. |
BufferRows |
int (bindable) |
2 | Overscan rows realized above and below the viewport. |
RealizedCellCount |
int (read-only) |
— | Diagnostics: current pool-in count, for verifying bounded realization. |
Grid example
xmlns:tblv="clr-namespace:TheBetterListView.MAUI;assembly=TheBetterListView.MAUI"
<RefreshView Refreshing="OnRefreshing">
<tblv:BetterItemsView ItemsSource="{Binding Items}"
ColumnCount="{OnIdiom Phone=3, Default=4}"
ItemHeight="155" RowSpacing="12" ColumnSpacing="12">
<tblv:BetterItemsView.ItemTemplate>
<DataTemplate x:DataType="models:MyItem">
<Border Margin="10">
<Grid RowDefinitions="*,20">
<Image Source="{Binding Icon}" Aspect="AspectFit" HeightRequest="100" />
<Label Grid.Row="1" Text="{Binding Title}" LineBreakMode="TailTruncation" />
</Grid>
</Border>
</DataTemplate>
</tblv:BetterItemsView.ItemTemplate>
</tblv:BetterItemsView>
</RefreshView>
List example
<tblv:BetterItemsView ItemsSource="{Binding Items}" ColumnCount="1" ItemHeight="70">
<tblv:BetterItemsView.ItemTemplate>
<DataTemplate x:DataType="models:MyItem">
<Grid Padding="0,5" ColumnDefinitions="70,*">
<Image Source="{Binding Icon}" WidthRequest="30" HeightRequest="30" />
<Label Grid.Column="1" Text="{Binding Title}" VerticalOptions="Center" />
</Grid>
</DataTemplate>
</tblv:BetterItemsView.ItemTemplate>
</tblv:BetterItemsView>
Swapping in for an existing CollectionView
The binding surface is intentionally close, so a swap is mostly mechanical:
- Keep any wrapping
RefreshView— pull-to-refresh works because the control's visual tree contains a native scroll view. - Move the
DataTemplateover verbatim — gestures (TapGestureRecognizer,PointerGestureRecognizer),DataTriggers, badges, and converters all keep working; they live in plain MAUI views. - Map
GridItemsLayout Span→ColumnCount,Horizontal/VerticalItemSpacing→ColumnSpacing/RowSpacing.LinearItemsLayout→ColumnCount="1". - Pick an explicit
ItemHeightfrom the template's known structure (e.g. image 100 + title 20 + paddings/margins). This replaces auto-sizing — that determinism is the fix. - Code that reads
ItemsSourceback off the view keeps working — the property returns the same object instance you assigned. - Converters that receive the view via
ConverterParameter={x:Reference ...}and cast toCollectionViewmust learn to acceptBetterItemsViewtoo (itsItemsSourceproperty is all they usually need).
Behavior notes
- Reassigning
ItemsSource(e.g. navigating between folders) resets scroll to the top. ItemTemplatemust produce aView; aCellthrowsInvalidOperationException.- All items share one cell size. Uneven/self-sized content inside the cell is fine (it lays out within the fixed frame); varying cell heights are out of scope by design.
- Collection changes trigger a cheap rebind of visible cells only (a few dozen views), regardless of collection size.
Development
Tests/— xUnit suite for the virtualization math (VirtualGridCalculator) and source tracking (ItemsSourceTracker):cd Tests && dotnet test.- Sibling repo
TheBetterListViewTestApp— interactive harness: 300 mixed items, grid/list toggle, long-press selection, pull-to-refresh, wholesale source swaps, realized-cell diagnostics.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-android36.0 is compatible. net10.0-browser was computed. net10.0-ios was computed. net10.0-ios26.0 is compatible. net10.0-maccatalyst was computed. net10.0-maccatalyst26.0 is compatible. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net10.0-windows10.0.19041 is compatible. |
-
net10.0
- Microsoft.Maui.Controls (>= 10.0.40)
-
net10.0-android36.0
- Microsoft.Maui.Controls (>= 10.0.40)
-
net10.0-ios26.0
- Microsoft.Maui.Controls (>= 10.0.40)
-
net10.0-maccatalyst26.0
- Microsoft.Maui.Controls (>= 10.0.40)
-
net10.0-windows10.0.19041
- Microsoft.Maui.Controls (>= 10.0.40)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.