VirtualScroll

A high-performance virtualized scrolling view designed to replace the traditional CollectionView in .NET MAUI applications.
VirtualScroll provides a more efficient implementation tailored specifically for Android and iOS platforms, offering smooth scrolling, dynamic item sizing, and proper support for observable collections.
VirtualScroll is the result of significant platform-level work and performance learnings—including over a year of hands-on experience gained by contributing improvements to .NET MAUI core as a community contributor.
Note: This package uses a Non-Commercial License. Please refer to the LICENSE-VirtualScroll.md for details.
I'm evaluating whether to relicense VirtualScroll under MIT (including commercial use) in the future, based on the level of community support and donations.
If this library is valuable to your work, consider supporting its continued development and maintenance through a donation:
Getting Started
Add VirtualScroll to your application in MauiProgram.cs:
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseNaluVirtualScroll(); // Add this line
Basic Usage
The simplest way to use VirtualScroll is to bind it to an ObservableCollection<T>:
<nalu:VirtualScroll Adapter="{Binding Items}">
<nalu:VirtualScroll.ItemTemplate>
<DataTemplate x:DataType="models:MyItem">
<nalu:ViewBox>
<Label Text="{Binding Name}" Padding="16" />
</nalu:ViewBox>
</DataTemplate>
</nalu:VirtualScroll.ItemTemplate>
</nalu:VirtualScroll>
The Adapter property accepts:
ObservableCollection<T>: Automatically wrapped with full change notification support (add, remove, move, replace, reset)IEnumerable: Static lists are supported but won't react to changesIVirtualScrollAdapter: Custom adapters for advanced scenarios like sectioned data
Factory Methods
For programmatic adapter creation, VirtualScroll provides factory methods that offer type-safe ways to create adapters:
Observable Collection Adapters
Create adapters for observable collections with full change notification support:
// Single collection
var adapter = VirtualScroll.CreateObservableCollectionAdapter(items);
// Read-only observable collection
var adapter = VirtualScroll.CreateObservableCollectionAdapter(readOnlyItems);
// Grouped collections
var adapter = VirtualScroll.CreateObservableCollectionAdapter(
sections,
section => section.Items);
The factory methods support various combinations of ObservableCollection<T> and ReadOnlyObservableCollection<T> for both sections and items.
Static Collection Adapters
Create adapters for static collections (no change notifications):
// Single collection
var adapter = VirtualScroll.CreateStaticCollectionAdapter(items);
// Grouped collections
var adapter = VirtualScroll.CreateStaticCollectionAdapter(
sections,
section => section.Items);
Static collection adapters are useful when:
- Your data doesn't change after initial load
- You want to use
IEnumerable<T>collections like arrays or LINQ results - You're working with grouped data that doesn't need change notifications
Example usage:
public partial class MyPageModel : ObservableObject
{
public IVirtualScrollAdapter Adapter { get; }
public MyPageModel()
{
// Create grouped adapter from static data
var categories = new[]
{
new Category { Name = "A", Items = new[] { new Item("A1"), new Item("A2") } },
new Category { Name = "B", Items = new[] { new Item("B1"), new Item("B2") } }
};
Adapter = VirtualScroll.CreateStaticCollectionAdapter(
categories,
category => category.Items);
}
}
Templates
VirtualScroll supports multiple template types to create rich scrolling experiences:
ItemTemplate
The template used to display each item in the collection:
<nalu:VirtualScroll.ItemTemplate>
<DataTemplate x:DataType="models:Person">
<nalu:ViewBox>
<Border StrokeShape="RoundRectangle 8" Margin="8" Padding="16">
<Label Text="{Binding FullName}" />
</Border>
</nalu:ViewBox>
</DataTemplate>
</nalu:VirtualScroll.ItemTemplate>
Tip: Wrap your item content in a
nalu:ViewBoxfor optimal performance.ViewBoxis a lightweight alternative toContentViewthat doesn't rely on the legacy Xamarin Compatibility layout system.
HeaderTemplate & FooterTemplate
Display content at the very beginning and end of the scroll view:
<nalu:VirtualScroll Adapter="{Binding Items}">
<nalu:VirtualScroll.HeaderTemplate>
<DataTemplate x:DataType="pageModels:MyPageModel">
<VerticalStackLayout>
<Image Source="banner.png" HeightRequest="128" Aspect="AspectFit" />
<Label Text="Welcome" FontSize="32" FontAttributes="Bold" HorizontalOptions="Center" />
</VerticalStackLayout>
</DataTemplate>
</nalu:VirtualScroll.HeaderTemplate>
<nalu:VirtualScroll.FooterTemplate>
<DataTemplate x:DataType="pageModels:MyPageModel">
<Label Text="{Binding FooterMessage}" Padding="16" HorizontalOptions="Center" />
</DataTemplate>
</nalu:VirtualScroll.FooterTemplate>
<nalu:VirtualScroll.ItemTemplate>
<!-- Item template here -->
</nalu:VirtualScroll.ItemTemplate>
</nalu:VirtualScroll>
SectionHeaderTemplate & SectionFooterTemplate
For sectioned data (when using a custom IVirtualScrollAdapter), you can define templates for section headers and footers:
<nalu:VirtualScroll.SectionHeaderTemplate>
<DataTemplate x:DataType="models:Section">
<Label Text="{Binding Title}" FontSize="18" FontAttributes="Bold" BackgroundColor="LightGray" Padding="16,8" />
</DataTemplate>
</nalu:VirtualScroll.SectionHeaderTemplate>
<nalu:VirtualScroll.SectionFooterTemplate>
<DataTemplate x:DataType="models:Section">
<BoxView HeightRequest="1" BackgroundColor="Gray" />
</DataTemplate>
</nalu:VirtualScroll.SectionFooterTemplate>
DataTemplateSelector Support
All templates support DataTemplateSelector for heterogeneous item types:
<nalu:VirtualScroll.ItemTemplate>
<local:MyItemTemplateSelector
TextTemplate="{StaticResource TextItemTemplate}"
ImageTemplate="{StaticResource ImageItemTemplate}" />
</nalu:VirtualScroll.ItemTemplate>
Layouts
The ItemsLayout property controls how items are arranged. Currently, VirtualScroll supports linear layouts:
<!-- Vertical scrolling (default) -->
<nalu:VirtualScroll ItemsLayout="{x:Static nalu:LinearVirtualScrollLayout.Vertical}" ... />
<!-- Horizontal scrolling -->
<nalu:VirtualScroll ItemsLayout="{x:Static nalu:LinearVirtualScrollLayout.Horizontal}" ... />
Scroll To Item
VirtualScroll provides methods to programmatically scroll to specific items. See Scrolling for details.
Scroll Events
VirtualScroll provides two ways to respond to scroll position changes. See Scrolling for details.
Visible Items Range
Get the range of currently visible items (including headers and footers). See Scrolling for details.
Pull-to-Refresh
Enable pull-to-refresh functionality with the following properties:
<nalu:VirtualScroll Adapter="{Binding Items}"
IsRefreshEnabled="True"
RefreshCommand="{Binding RefreshCommand}"
RefreshAccentColor="CornflowerBlue"
IsRefreshing="{Binding IsLoading}">
...
</nalu:VirtualScroll>
Properties
| Property | Type | Description |
|---|---|---|
IsRefreshEnabled |
bool |
Enables or disables pull-to-refresh. Default: false |
RefreshCommand |
ICommand |
Command executed when the user triggers a refresh. The command receives a completion callback as parameter. |
RefreshAccentColor |
Color |
The color of the refresh indicator |
IsRefreshing |
bool |
Two-way bindable property indicating whether the refresh is in progress |
RefreshCommand Implementation
The RefreshCommand receives a completion callback that must be invoked when the refresh operation completes:
[RelayCommand]
private async Task RefreshAsync(Action completionCallback)
{
try
{
await LoadDataAsync();
}
finally
{
completionCallback(); // Always call this when done!
}
}
OnRefresh Event
Alternatively, you can handle the OnRefresh event:
virtualScroll.OnRefresh += async (sender, args) =>
{
await LoadDataAsync();
args.Complete(); // Signal completion
};
Fading Edge
VirtualScroll supports a fading edge effect that creates a smooth gradient at the scrollable edges, providing visual feedback about scrollable content. The fading edge automatically adapts to the scroll direction based on the ItemsLayout orientation.
<!-- Vertical scrolling with fading edge -->
<nalu:VirtualScroll Adapter="{Binding Items}"
ItemsLayout="{x:Static nalu:LinearVirtualScrollLayout.Vertical}"
FadingEdgeLength="16">
...
</nalu:VirtualScroll>
<!-- Horizontal scrolling with fading edge -->
<nalu:VirtualScroll Adapter="{Binding Items}"
ItemsLayout="{x:Static nalu:LinearVirtualScrollLayout.Horizontal}"
FadingEdgeLength="24">
...
</nalu:VirtualScroll>
How It Works
- Vertical layouts: The fading edge appears at the top and/or bottom edges when content extends beyond the visible area
- Horizontal layouts: The fading edge appears at the left and/or right edges when content extends beyond the visible area
- The fading edge automatically appears/disappears based on scroll position:
- When scrolled to the start, only the end edge shows fading
- When scrolled to the end, only the start edge shows fading
- When in the middle, both edges show fading
- When all content fits in the view, no fading edge is shown
Properties
| Property | Type | Default | Description |
|---|---|---|---|
FadingEdgeLength |
double |
0.0 |
The length of the fading edge effect in device-independent units. A value of 0 disables the fading edge. The orientation (horizontal or vertical) is automatically determined from the ItemsLayout. |
Note: The fading edge feature is optimized for Android and iOS platforms. On Android, it uses native
RecyclerViewfading edge support. On iOS, it uses a custom gradient mask implementation.
Custom Adapters
For advanced scenarios requiring sectioned data or direct data source access, implement IVirtualScrollAdapter. See Custom Adapters for complete documentation.
Dynamic Item Sizing
VirtualScroll fully supports dynamic item sizes. Items can change their height/width at runtime, and the scroll view will automatically adjust:
<DataTemplate x:DataType="models:ExpandableItem">
<nalu:ViewBox>
<Border Margin="8" Padding="16">
<VerticalStackLayout>
<Label Text="{Binding Title}" />
<!-- Content that may change size -->
<Label Text="{Binding Description}" IsVisible="{Binding IsExpanded}" />
</VerticalStackLayout>
<Border.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ToggleExpandCommand}" />
</Border.GestureRecognizers>
</Border>
</nalu:ViewBox>
</DataTemplate>
Complete Example
Here's a complete example demonstrating VirtualScroll with header, footer, dynamic items, and scroll functionality:
XAML:
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:nalu="https://nalu-development.github.com/nalu/layouts"
xmlns:pageModels="clr-namespace:MyApp.PageModels"
x:Class="MyApp.Pages.ItemListPage"
x:DataType="pageModels:ItemListPageModel"
Title="Items">
<Grid RowDefinitions="Auto,*">
<!-- Toolbar -->
<HorizontalStackLayout Grid.Row="0" Spacing="8" Padding="16,8">
<Button Text="Add" Command="{Binding AddItemCommand}" />
<Button Text="Remove" Command="{Binding RemoveItemCommand}" />
<Button Text="Scroll" Command="{Binding ScrollToRandomCommand}" />
</HorizontalStackLayout>
<!-- VirtualScroll -->
<nalu:VirtualScroll Grid.Row="1"
x:Name="VirtualScroll"
Adapter="{Binding Items}"
IsRefreshEnabled="True"
RefreshCommand="{Binding RefreshCommand}">
<nalu:VirtualScroll.HeaderTemplate>
<DataTemplate x:DataType="pageModels:ItemListPageModel">
<VerticalStackLayout Padding="16">
<Image Source="logo.png" HeightRequest="100" Aspect="AspectFit" />
<Label Text="My Items" FontSize="28" FontAttributes="Bold" HorizontalOptions="Center" />
</VerticalStackLayout>
</DataTemplate>
</nalu:VirtualScroll.HeaderTemplate>
<nalu:VirtualScroll.FooterTemplate>
<DataTemplate x:DataType="pageModels:ItemListPageModel">
<Label Text="{Binding ItemCount, StringFormat='Total: {0} items'}"
Padding="16"
HorizontalOptions="Center"
TextColor="Gray" />
</DataTemplate>
</nalu:VirtualScroll.FooterTemplate>
<nalu:VirtualScroll.ItemTemplate>
<DataTemplate x:DataType="pageModels:ItemViewModel">
<nalu:ViewBox>
<Border StrokeShape="RoundRectangle 8"
Margin="8"
Padding="16"
BackgroundColor="LightCoral">
<Label Text="{Binding Name}" />
<Border.GestureRecognizers>
<TapGestureRecognizer Command="{Binding TapCommand}" />
</Border.GestureRecognizers>
</Border>
</nalu:ViewBox>
</DataTemplate>
</nalu:VirtualScroll.ItemTemplate>
</nalu:VirtualScroll>
</Grid>
</ContentPage>
PageModel:
public partial class ItemListPageModel : ObservableObject
{
public ObservableCollection<ItemViewModel> Items { get; } = new();
public int ItemCount => Items.Count;
[RelayCommand]
private void AddItem()
{
var index = Items.Count > 0 ? Random.Shared.Next(Items.Count) : 0;
Items.Insert(index, new ItemViewModel($"Item {Items.Count + 1}"));
OnPropertyChanged(nameof(ItemCount));
}
[RelayCommand]
private void RemoveItem()
{
if (Items.Count > 0)
{
Items.RemoveAt(Random.Shared.Next(Items.Count));
OnPropertyChanged(nameof(ItemCount));
}
}
[RelayCommand]
private async Task RefreshAsync(Action completionCallback)
{
await Task.Delay(1000); // Simulate loading
completionCallback();
}
}
Code-Behind (for ScrollTo):
public partial class ItemListPage : ContentPage
{
public ItemListPage(ItemListPageModel viewModel)
{
BindingContext = viewModel;
InitializeComponent();
}
public void ScrollToItem(int index)
{
VirtualScroll.ScrollTo(0, index, ScrollToPosition.Center);
}
}
API Reference
For complete API documentation including all properties, methods, and events, see the VirtualScroll API Reference.
Platform Support
VirtualScroll is optimized for:
- Android - Uses
RecyclerViewunder the hood - iOS / Mac Catalyst - Uses
UICollectionViewunder the hood
Windows support is not currently available.
Performance Comparison with MAUI CollectionView
VirtualScroll is designed to provide superior performance compared to MAUI's built-in CollectionView. See Performance for detailed benchmarks and optimization tips.
Learn More
- 📘 Scrolling - Scroll to item, scroll events, and visible items range
- 📘 Custom Adapters - Creating custom adapters for sectioned data and database-backed lists
- 📘 Performance - Performance benchmarks and optimization tips