ListView
ListView is a full-featured data grid for listing entities. It loads paged OData data and supports live search, column selection, advanced filtering, grouping, multi-column sorting, inline editing, row selection, and named saved views. It requires a ListViewDefinition (built with ListViewBuilder) and renders inside a standard Blazor page without requiring a DataGraph.
ListViewDefinition and ListViewBuilder
ListViewBuilder is a fluent builder that produces a ListViewDefinition. You can construct one from a plain entity name or from an existing GraphDefinition.
Builder API
| Method | Description |
|---|---|
Column(path) |
Add a visible column by property path |
Column(path, template) |
Add a visible column with a custom cell template |
Columns(paths) |
Add multiple visible columns |
ColumnTemplate(path, template) |
Attach a custom cell template to an existing column |
HiddenColumn(path) |
Fetch the path from OData but hide it in the grid UI |
ReadOnlyColumn(path) |
Prevent inline editing for a column even when metadata allows it |
ReadOnlyColumns(paths) |
Mark multiple columns as read-only |
Graph(graphDefinition) |
Replace the underlying graph, keeping already-added columns |
Graph(configure) |
Configure the graph with a builder action |
Filter(filter) |
Set a permanent OData $filter expression applied to every request |
OrderBy(orderBy) |
Set the default sort order (e.g., "Name asc, CreatedAt desc") |
Search(text) |
Set an initial full-text search string |
View(savedView) |
Register a code-defined saved view preset |
Views(savedViews) |
Register multiple code-defined saved view presets |
FavoriteProperty(path) |
Add a property path to the default favorite filter list |
FavoriteProperties(paths) |
Add multiple paths to the favorite filter list |
Build() |
Produce a ListViewDefinition |
Example
private static readonly ListViewDefinition CustomerListDefinition =
new ListViewBuilder("Customer")
.Column("Number")
.Column("Name")
.Column("PrimaryContact.PrimaryEmail")
.HiddenColumn("StatusCode") // loaded but not shown; available for custom templates
.ReadOnlyColumn("Number")
.Filter("IsArchived eq false")
.OrderBy("Name asc")
.FavoriteProperties(["Status", "Region"])
.View(new ListViewSavedView
{
Id = "active-only",
Name = "Active",
Filters =
[
new ListViewFilterDraft
{
Path = "Status",
ClrType = "string",
Operator = "eq",
Value = "Active",
DisplayValue = "Active"
}
]
})
.Build();
The ListViewDefinition.EntityName is derived from the graph's RootEntity. Columns returns the visible paths in declaration order, excluding hidden columns and any path ending in Guid.
ListView Parameters
Place a <ListView> component on any Blazor page and pass the definition. No DataGraph is required.
<ListView Definition="@_definition" ViewKey="customers" OnRowClick="@NavigateToCustomer" />
Parameter Reference
| Parameter | Type | Default | Description |
|---|---|---|---|
Definition |
ListViewDefinition (required) |
— | Defines entity, columns, filters, ordering, and view presets |
ViewKey |
string? |
null |
Scopes localStorage state (columns, filters, saved views) per page. Use a unique string per list on the same entity |
EnableSearch |
bool |
true |
Show the search bar |
EnableFiltering |
bool |
true |
Show the filter panel and active filter chips |
EnableColumnSelection |
bool |
true |
Allow the user to add, remove, and reorder columns |
EnableSavedViews |
bool |
true |
Show the saved views menu |
EnableSelection |
bool |
false |
Show row checkboxes and enable multi-row selection |
EnableInlineEditing |
bool |
true |
Allow clicking cells to edit values in place |
UseVirtualization |
bool |
true |
Use Blazor Virtualize for large result sets |
ItemSize |
float |
36f |
Row height in pixels, used by the virtualizer |
MaxHeight |
string? |
"85vh" |
CSS height/max-height for the grid container. Pass null for no cap |
MaxNonVirtualizedRows |
int |
1000 |
Row limit before the non-virtualized renderer shows an error instead of the data |
OnRowClick |
EventCallback<Guid> |
— | Fired with the row's Guid when the user clicks a data row |
GroupHeaderClicked |
EventCallback<ListViewGroupHeaderClickArgs> |
— | Fired when the user clicks a group header row |
GroupHeaderFirstRowClicked |
EventCallback<JsonObject> |
— | Fired with the first row's JsonObject when the user clicks a group header in single-select mode |
SelectedRowsChanged |
EventCallback<IReadOnlyList<JsonObject>> |
— | Fired with the full JsonObject for every selected row |
SelectedGuidsChanged |
EventCallback<IReadOnlyList<Guid>> |
— | Fired with the Guid of every selected row |
CaptureCustomFilterState |
Func<string?>? |
null |
Called when a saved view snapshot is taken; return a serialized string representing any custom filter state on the page |
RestoreCustomFilterStateRequested |
EventCallback<string?> |
— | Fired when a saved view is loaded; the argument is the previously captured custom filter state string, or null to clear |
ChildContent |
RenderFragment? |
null |
Declarative <ColumnTemplate> elements |
Column Templates
Custom cell rendering can be provided in two ways.
Declarative syntax
Place <ColumnTemplate> elements inside the ListView component body. The PropertyPath attribute must match a path registered in the definition.
<ListView Definition="@_definition" ViewKey="customers">
<ColumnTemplate PropertyPath="Status">
<span class="chip chip--@context.Value?.ToString()?.ToLower()">
@context.DisplayValue
</span>
</ColumnTemplate>
</ListView>
The context variable is a ListViewCellRenderContext.
Dictionary parameter
Supply templates on the definition itself using ListViewBuilder.ColumnTemplate or by passing a dictionary to ListViewDefinition.ColumnTemplates. The key is the property path (case-insensitive); the value is a RenderFragment<ListViewCellRenderContext>.
.Column("Status", context =>
builder => builder.AddContent(0, context.DisplayValue.ToUpper()))
ListViewCellRenderContext
| Member | Type | Description |
|---|---|---|
Row |
JsonObject |
The full row data as a JSON object |
Column |
string |
The property path being rendered |
Label |
string |
The display label for the column |
Value |
JsonNode? |
The raw JSON node for the cell value |
DisplayValue |
string |
The default formatted string for the value |
Property |
PropertyMetaData? |
Model metadata for the column, when available |
Choices |
IReadOnlyList<string> |
Configured display choices for enum columns |
OnRowClick |
Func<Task>? |
Invoke to trigger the OnRowClick callback from inside the template |
Get(path) |
JsonNode? |
Read another property path from the same row |
Text(path) |
string |
Read the formatted display value for another path in the same row |
Filtering
When EnableFiltering is true, the search bar renders active filter chips and an "Add filter" button that opens the filter panel. Each filter rule consists of a property path, an operator, and an optional value. The filter compiler converts rules to OData $filter expressions and combines them with and.
Operators by type
String — eq (Is), ne (Is not), contains, startswith (Begins with), endswith (Ends with), empty (Is empty), notempty (Is not empty)
Number — eq (Equals), ne (Does not equal), gt (Greater than), ge (Greater than or eq), lt (Less than), le (Less than or eq), empty, notempty
Boolean — istrue (Is true), isfalse (Is false), empty, notempty
Date — on, noton, between, today, thisweek, lastweek, nextweek, thismonth, inlast (In last N days), innext (In next N days), startingafter, priorto, after, onorafter, before, onorbefore, empty, notempty
Reference — eq (Is), ne (Is not), empty, notempty. Reference filters resolve to the FK Guid path on the server.
Enum — eq (Is), ne (Is not)
Default filters in the definition
Pass a raw OData $filter string to ListViewBuilder.Filter. This filter is always applied and is invisible to the user — it is not shown in the filter bar and cannot be removed by the user. Use it to scope the list to a subset of data (e.g., excluding archived records).
new ListViewBuilder("Invoice")
.Filter("IsDeleted eq false")
...
User-applied filters from the filter panel are compiled separately and combined with the definition filter using and.
Grouping
When a group field is active, the grid renders group header rows. Rows inside each group can be expanded or collapsed. The active group is stored in session state under the ViewKey scope.
ListViewGroupField
| Property | Description |
|---|---|
SelectedPath |
The path the user selected in the group picker (e.g., "Status" or "Region") |
ResolvedPath |
The OData path used in the query (e.g., "RegionGuid" for reference fields) |
HeaderDisplayPaths |
Property paths read from the group header row to form the header label |
IsReference |
Whether the group field is a navigation property |
Label |
Display label shown in the group picker UI |
Setting a default group in the definition
Supply a default group by passing a ListViewGroupField in a ListViewSavedView.GroupBy in the first registered view, or by applying it programmatically through the filter panel state. The component picks up the persisted group from session state on load.
Group header click events
GroupHeaderClicked fires a ListViewGroupHeaderClickArgs with information about the group header that was clicked. GroupHeaderFirstRowClicked fires with the JsonObject of the first row in the group, useful for navigating to the first matching entity.
<ListView Definition="@_definition"
ViewKey="orders"
GroupHeaderFirstRowClicked="@OnGroupHeaderFirstRowClicked" />
@code {
private void OnGroupHeaderFirstRowClicked(JsonObject firstRow)
{
if (firstRow["Guid"]?.GetValue<Guid>() is Guid guid)
Navigation.NavigateTo($"/orders/{guid}");
}
}
Sorting
Set the default sort with ListViewBuilder.OrderBy. The value is an OData-style $orderby string:
.OrderBy("CreatedAt desc, Name asc")
Users can click column headers to toggle sorts. The first click applies an ascending sort; the second toggles to descending; the third removes the sort. Multiple columns can be sorted simultaneously — each subsequent click on a new column appends it to the active sort list. The sort index (1, 2, 3, …) is shown in the header alongside the direction indicator.
Row Selection
Set EnableSelection="true" to show a checkbox column on the left. Selecting a row adds it to the internal selection state. The select-all checkbox in the header fetches all rows matching the current filter (not just the loaded page) and selects them.
SelectedGuidsChanged fires with IReadOnlyList<Guid> — the Guid value from each selected row.
SelectedRowsChanged fires with IReadOnlyList<JsonObject> — the full row data for each selected row, as last loaded from the server.
<ListView Definition="@_definition"
ViewKey="invoices"
EnableSelection="true"
SelectedGuidsChanged="@OnSelectionChanged" />
@code {
private IReadOnlyList<Guid> _selectedGuids = [];
private void OnSelectionChanged(IReadOnlyList<Guid> guids)
=> _selectedGuids = guids;
}
Selection state is cleared whenever the filter, search, or data version changes.
Saved Views
Saved views capture a complete snapshot of the current grid state: active filters, visible columns, column widths, sort order, grouping, and search text. Users can create and load views through the saved views menu when EnableSavedViews is true.
ViewKey is required to persist saved views. Without it, state is not stored between navigation events.
ListViewSavedView snapshot contents
| Property | Description |
|---|---|
Id |
Unique identifier (stable string; use Guid.NewGuid().ToString("N") for user-created views) |
Name |
Display name shown in the menu |
Search |
Captured search text |
Filters |
List of ListViewFilterDraft rules |
Columns |
Ordered list of visible column paths |
ColumnWidths |
Dictionary of column path to pixel width |
OrderBy |
OData-style sort string |
GroupBy |
Active group field snapshot |
CustomFilter |
Serialized OData filter from the page's custom filter (managed by the component) |
CustomFilterLabel |
Display label for the custom filter chip |
CustomFilterState |
Arbitrary serialized state returned by CaptureCustomFilterState |
Code-defined view presets
Use ListViewBuilder.View to register views that always appear in the menu. When a user modifies a code-defined view, their changes are stored separately; the original definition remains available as a reset target.
new ListViewBuilder("WorkOrder")
.View(new ListViewSavedView
{
Id = "open-orders",
Name = "Open",
Filters =
[
new ListViewFilterDraft
{
Path = "Status",
ClrType = "string",
Operator = "ne",
Value = "Closed",
DisplayValue = "Closed"
}
]
})
Custom filter state hook
Pages that maintain filter state outside the built-in filter bar — such as a date range picker in the page header — can participate in saved views through the two custom filter state parameters.
CaptureCustomFilterState is called when the view is saved. Return a serialized string (JSON, a query string fragment, or any format your page understands).
RestoreCustomFilterStateRequested fires when a view is loaded or cleared. Apply the serialized state back to your page controls.
<ListView Definition="@_definition"
ViewKey="shipments"
EnableSavedViews="true"
CaptureCustomFilterState="@CaptureFilterState"
RestoreCustomFilterStateRequested="@RestoreFilterState" />
@code {
private DateOnly? _from;
private DateOnly? _to;
private string? CaptureFilterState()
=> (_from, _to) is (not null, not null)
? $"{_from:yyyy-MM-dd}|{_to:yyyy-MM-dd}"
: null;
private async Task RestoreFilterState(string? state)
{
if (state is null)
{
_from = null;
_to = null;
}
else
{
var parts = state.Split('|');
_from = DateOnly.TryParse(parts[0], out var f) ? f : null;
_to = parts.Length > 1 && DateOnly.TryParse(parts[1], out var t) ? t : null;
}
await ApplyDateFilterAsync();
}
}
Inline Editing
When EnableInlineEditing is true, clicking an editable cell opens an in-place text input. Press Enter or Tab to commit; Escape cancels. Tab commits the current cell and moves to the next editable cell in reading order.
A column is editable when all of the following are true:
EnableInlineEditingistrue- The path is a single-segment path (no
.) - The path is not
Guid - The server metadata marks the property as writable (not
ReadOnly) - The path is not in
ListViewDefinition.ReadOnlyColumns - The property is not virtual, a navigation, or a collection
Committed changes are sent as an OData PATCH request. Validation errors returned by the server are displayed as an inline error on the cell. After a successful save, the row is refreshed from the server.
Virtualization
UseVirtualization (default true) enables Blazor's Virtualize component for large lists. The virtualizer fetches rows in blocks of 100 as the user scrolls. Set ItemSize to match the actual rendered row height in pixels; the default of 36f matches the grid's compact row style.
When UseVirtualization is false, the component loads all matching rows up to MaxNonVirtualizedRows in a single request. If the result count exceeds MaxNonVirtualizedRows, the grid displays an error instead of truncated data.
When grouping is active the component always operates in non-virtualized mode inside each group section, regardless of UseVirtualization.
Use virtualization when the list can return more than a few hundred rows under normal filter conditions. Disable it when the total row count is small and stable, or when you need to render the grid inside a container that measures the total content height.
Code Examples
1. Minimal customer list
// CustomerList.razor.cs
private static readonly ListViewDefinition Definition =
new ListViewBuilder("Customer")
.Column("Number")
.Column("Name")
.Column("PrimaryContact.PrimaryEmail")
.OrderBy("Name asc")
.Build();
@* CustomerList.razor *@
@page "/customers"
@inject NavigationManager Navigation
<ListView Definition="@Definition"
ViewKey="customers"
OnRowClick="@(guid => Navigation.NavigateTo($"/customers/{guid}"))" />
2. List with search, filtering, and column selection
<ListView Definition="@Definition"
ViewKey="invoices"
EnableSearch="true"
EnableFiltering="true"
EnableColumnSelection="true"
EnableSavedViews="false"
OnRowClick="@NavigateToInvoice" />
3. Custom column template — status chip
private static readonly ListViewDefinition Definition =
new ListViewBuilder("WorkOrder")
.Column("Number")
.Column("Title")
.Column("Status")
.Column("AssignedTo.FullName")
.Build();
<ListView Definition="@Definition" ViewKey="work-orders">
<ColumnTemplate PropertyPath="Status">
<span class="status-chip status-chip--@context.Value?.ToString()?.ToLower()">
@context.DisplayValue
</span>
</ColumnTemplate>
</ListView>
context.Value contains the raw JsonNode for the cell; context.DisplayValue is the pre-formatted string. Both are available inside the template.
4. Grouped list with a default group
private static readonly ListViewDefinition Definition =
new ListViewBuilder("ServiceTicket")
.Column("Number")
.Column("Subject")
.Column("Priority")
.Column("Status")
.View(new ListViewSavedView
{
Id = "by-status",
Name = "By Status",
GroupBy = new ListViewGroupField
{
SelectedPath = "Status",
ResolvedPath = "Status",
ResolvedClrType = "string",
Label = "Status"
}
})
.Build();
<ListView Definition="@Definition"
ViewKey="tickets"
EnableSavedViews="true"
GroupHeaderClicked="@OnGroupClicked" />
The first registered view is applied automatically on first load. Users can change or clear the grouping through the filter panel.
5. Row selection with SelectedGuidsChanged
<ListView Definition="@Definition"
ViewKey="products"
EnableSelection="true"
SelectedGuidsChanged="@OnSelectionChanged" />
<button disabled="@(!_selectedGuids.Any())" @onclick="BulkArchive">
Archive @_selectedGuids.Count selected
</button>
@code {
private IReadOnlyList<Guid> _selectedGuids = [];
private void OnSelectionChanged(IReadOnlyList<Guid> guids)
=> _selectedGuids = guids;
private async Task BulkArchive()
{
foreach (var guid in _selectedGuids)
await ArchiveAsync(guid);
}
}
6. Saved views with a custom filter state hook
This example shows a page that has a region selector outside the built-in filter bar. The selected region is captured into saved views and restored when a view is loaded.
@page "/orders"
@inject NavigationManager Navigation
<select @bind="_regionCode">
<option value="">All regions</option>
<option value="US-WEST">West</option>
<option value="US-EAST">East</option>
</select>
<ListView @ref="_listView"
Definition="@Definition"
ViewKey="orders-by-region"
EnableSavedViews="true"
CaptureCustomFilterState="@CaptureState"
RestoreCustomFilterStateRequested="@RestoreState"
OnRowClick="@(guid => Navigation.NavigateTo($"/orders/{guid}"))" />
@code {
private ListView? _listView;
private string _regionCode = string.Empty;
private static readonly ListViewDefinition Definition =
new ListViewBuilder("Order")
.Column("Number")
.Column("Customer.Name")
.Column("OrderDate")
.Column("Total")
.OrderBy("OrderDate desc")
.Build();
private string? CaptureState()
=> string.IsNullOrWhiteSpace(_regionCode) ? null : _regionCode;
private async Task RestoreState(string? state)
{
_regionCode = state ?? string.Empty;
await ApplyRegionFilterAsync();
}
private async Task ApplyRegionFilterAsync()
{
if (_listView is null)
return;
var filter = string.IsNullOrWhiteSpace(_regionCode)
? null
: $"RegionCode eq '{_regionCode}'";
await _listView.ApplyCustomFilterAsync(filter, label: _regionCode);
}
}
ApplyCustomFilterAsync(filter, label) is a public method on ListView. Call it whenever your custom filter changes. The label parameter is shown as a chip in the filter bar so users can see the active custom filter without opening the filter panel.