Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

VirtualizedGrid

A high-performance virtualized grid that supports optional grouping with sticky headers, orientation-aware column counts, and built-in load-more pagination. Only the items currently visible in the viewport are created and measured, keeping memory use flat regardless of data set size.

  • NuGet downloads for Shiny.Maui.Controls
  • NuGet downloads for Shiny.Blazor.Controls
Frameworks
.NET MAUI
Blazor

MAUI

Flat (240) Grouped Load More Modes Load More Button
Flat virtualized grid with 240 items and adjustable column count Grouped virtualized grid with sticky headers Load-more mode toggle between button and threshold Load More button rendered at the end of the grid

Blazor

Flat grid Grouped with sticky headers Header, footer and load more
Flat grid on Blazor Grouped with sticky headers on Blazor Header, footer and load more on Blazor
  • Virtualization – Native recycling on Android (RecyclerView + GridLayoutManager), compositional layout on iOS (UICollectionViewCompositionalLayout), and ItemsRepeater + UniformGridLayout on Windows. Blazor uses the built-in <Virtualize> component with CSS Grid.
  • Sticky Group Headers – When IsGroupingEnabled is true, group headers pin to the top of the viewport as the user scrolls through a group.
  • Orientation-Aware Columns – Set PortraitColumnCount and LandscapeColumnCount independently so the layout adapts automatically on device rotation.
  • Load-More Pagination – A LoadMoreThreshold triggers LoadMoreRequested before the user reaches the last item; an optional ShowLoadMoreButton renders an explicit button for manual triggering.
  • Item Visibility EventsItemVisibleCommand and ItemHiddenCommand fire as items enter and leave the viewport (MAUI).
  • Flexible Templates – Header, footer, empty view, group header, and load-more button are all fully templatable.
claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View Skills Repository
<shiny:VirtualizedGrid ItemsSource="{Binding Products}"
PortraitColumnCount="2"
LandscapeColumnCount="4"
IsGroupingEnabled="True"
HasStickyHeaders="True"
CellPadding="8"
ShowLoadMoreButton="False"
IsLoadingMore="{Binding IsLoadingMore}"
ItemSelectedCommand="{Binding SelectProductCommand}">
<shiny:VirtualizedGrid.ItemTemplate>
<DataTemplate>
<Border StrokeShape="{RoundRectangle CornerRadius=8}" Padding="8">
<Label Text="{Binding Name}" />
</Border>
</DataTemplate>
</shiny:VirtualizedGrid.ItemTemplate>
<shiny:VirtualizedGrid.GroupHeaderTemplate>
<DataTemplate>
<Label Text="{Binding Key}" FontAttributes="Bold" FontSize="16" />
</DataTemplate>
</shiny:VirtualizedGrid.GroupHeaderTemplate>
</shiny:VirtualizedGrid>
<VirtualizedGrid Items="products"
ColumnCount="2"
ItemSpacing="8"
IsGroupingEnabled="true"
HasStickyHeaders="true"
EnableVirtualization="true"
LoadMoreThreshold="5"
IsLoadingMore="isLoadingMore"
LoadMoreRequested="OnLoadMore"
ItemSelected="OnProductSelected">
<ItemTemplate Context="product">
<div class="product-card">
<span>@product.Name</span>
</div>
</ItemTemplate>
<GroupHeaderTemplate Context="group">
<h3>@group</h3>
</GroupHeaderTemplate>
<EmptyViewTemplate>
<p>No products found.</p>
</EmptyViewTemplate>
</VirtualizedGrid>
@code {
List<Product> products = [];
bool isLoadingMore = false;
async Task OnLoadMore()
{
isLoadingMore = true;
// fetch next page...
isLoadingMore = false;
}
void OnProductSelected(Product product) { }
}
Property Type Default Description
ItemsSource IEnumerable Collection of items or grouped collections
ItemTemplate DataTemplate Template for each grid cell
ColumnCount int 1 Default column count
PortraitColumnCount int? null Column count in portrait orientation; overrides ColumnCount
LandscapeColumnCount int? null Column count in landscape orientation; overrides ColumnCount
CellPadding Thickness Padding applied inside each cell
IsGroupingEnabled bool false Enable grouped data with group headers
GroupHeaderTemplate DataTemplate Template for group header rows
HasStickyHeaders bool true Pin group headers to the top while scrolling
ShowLoadMoreButton bool false Render an explicit load-more button as a footer at the end of the data
LoadMoreButtonTemplate DataTemplate Custom template for the load-more button; defaults to a centered “Load More” button
IsLoadingMore bool false Indicates a load-more operation is in progress
ItemSelectedCommand ICommand Command fired when a cell is tapped
ItemVisibleCommand ICommand Command fired when a cell enters the viewport
ItemHiddenCommand ICommand Command fired when a cell leaves the viewport
Property Type Default Description
Items IReadOnlyList<TItem> Flat list of items
ItemTemplate RenderFragment<TItem> Template for each grid cell
HeaderTemplate RenderFragment Content rendered above the grid
FooterTemplate RenderFragment Content rendered below the grid
EmptyViewTemplate RenderFragment Content rendered when Items is empty
ColumnCount int 1 Number of grid columns
ItemSpacing double 8 Gap between cells in px
CellPaddingLeft double Left padding inside each cell
CellPaddingRight double Right padding inside each cell
CellPaddingTop double Top padding inside each cell
CellPaddingBottom double Bottom padding inside each cell
IsGroupingEnabled bool false Enable grouped rendering
GroupedItems IReadOnlyList<VirtualizedGridGroup<TItem>> Pre-grouped data source used when IsGroupingEnabled is true
GroupHeaderTemplate RenderFragment<object> Template for group header rows
HasStickyHeaders bool true Pin group headers while scrolling
EnableVirtualization bool false Use Blazor <Virtualize> for large datasets
LoadMoreThreshold int 5 Number of items from the end that triggers LoadMoreRequested
ShowLoadMoreButton bool false Render an explicit load-more button
IsLoadingMore bool false Indicates a load-more operation is in progress
LoadMoreRequested EventCallback Fired when the threshold is reached or the load-more button is clicked
ItemSelected EventCallback<TItem> Fired when a cell is clicked/tapped

MAUI:

  • ItemSelectedCommand (ICommand) – Fires on cell tap; parameter is the selected item
  • ItemVisibleCommand (ICommand) – Fires as a cell scrolls into the visible area
  • ItemHiddenCommand (ICommand) – Fires as a cell scrolls out of the visible area

Blazor:

  • LoadMoreRequested (EventCallback) – Fires when proximity threshold is reached or load-more button is pressed
  • ItemSelected (EventCallback<TItem>) – Fires on cell click/tap
  • When both PortraitColumnCount and LandscapeColumnCount are set, ColumnCount is ignored and the appropriate value is chosen based on current device orientation
  • Sticky headers require IsGroupingEnabled="true"; setting HasStickyHeaders="false" renders standard (non-sticky) group headers
  • LoadMoreThreshold counts from the last item; reaching that position fires LoadMoreRequested automatically even without ShowLoadMoreButton
  • Setting IsLoadingMore="true" shows a built-in activity indicator in the load-more area until the value returns to false
  • EnableVirtualization in Blazor requires Items to be provided as a flat list; use GroupedItems alongside IsGroupingEnabled for grouped virtualized rendering