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

Stringeq (Is), ne (Is not), contains, startswith (Begins with), endswith (Ends with), empty (Is empty), notempty (Is not empty)

Numbereq (Equals), ne (Does not equal), gt (Greater than), ge (Greater than or eq), lt (Less than), le (Less than or eq), empty, notempty

Booleanistrue (Is true), isfalse (Is false), empty, notempty

Dateon, 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

Referenceeq (Is), ne (Is not), empty, notempty. Reference filters resolve to the FK Guid path on the server.

Enumeq (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:

  • EnableInlineEditing is true
  • 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.