Accent color
# DataGrid
## Description
BitDataGrid is a robust way to display an information-rich collection of items, and allow people to sort and filter the content. Use a data-grid when information density is critical.
## Parameters
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Items | `IEnumerable<TItem>?` | null | The data source bound to the grid for client-side processing. An IQueryable<T> (e.g. an EF Core DbSet) gets filtering/sorting/paging translated into expression trees the provider executes at the source, materializing only the current page. |
| OnRead | `Func<BitDataGridReadRequest, Task<BitDataGridReadResult<TItem>>>?` | null | Server-side data callback. When set, the grid delegates sort/filter/page/group to the caller. |
| OnLoadMore | `Func<BitDataGridReadRequest, Task<BitDataGridReadResult<TItem>>>?` | null | Infinite-scrolling data callback. Loads rows in batches and appends the next batch as the user scrolls toward the end. CSV/Excel exports issue a single request with Take = null ("all rows"), so the handler should honor a null Take. |
| LoadMoreBatchSize | `int` | 50 | Number of rows fetched per batch in infinite-scrolling mode. |
| ChildContent | `RenderFragment?` | null | Column definitions and other declarative children. |
| Columns | `RenderFragment?` | null | Alias of ChildContent, letting column definitions read declaratively as <Columns>...</Columns>. Both fragments are rendered when both are set. |
| Loading | `bool` | false | Shows a loading overlay while data is being fetched. |
| KeyField | `Func<TItem, object>?` | null | Optional key selector used for selection/edit identity. Defaults to reference equality. |
| ChildrenSelector | `Func<TItem, IEnumerable<TItem>?>?` | null | Child selector that turns the grid into a hierarchical tree grid. |
| ChildrenProvider | `Func<TItem, Task<IEnumerable<TItem>?>>?` | null | Async children provider for a lazily-loaded tree grid: children are fetched on a node's first expand (e.g. from a backend) and cached. Mutually exclusive with ChildrenSelector; pair with HasChildrenSelector. |
| HasChildrenSelector | `Func<TItem, bool>?` | null | Tells whether a node can have children before they are loaded, so unloaded lazy nodes render an expand toggle. Only used with ChildrenProvider. |
| TreeInitiallyExpanded | `bool` | false | When tree mode is active, controls whether nodes start expanded. Ignored in lazy mode (ChildrenProvider). |
| Class | `string?` | null | Custom CSS class for the root element. |
| Style | `string?` | null | Custom inline style for the root element. |
| Height | `string?` | null | Height of the scroll viewport, e.g. "480px". Required for virtualization and infinite scrolling. |
| Striped | `bool` | true | Renders alternate-row striping. |
| Hoverable | `bool` | true | Highlights the row under the pointer. |
| Bordered | `bool` | true | Renders cell borders. |
| ShowHeader | `bool` | true | Renders the header row. |
| ShowFooter | `bool` | false | Renders the footer/aggregate row. |
| Direction | `BitDir` | BitDir.Ltr | Text direction (LTR/RTL). |
| Sortable | `bool` | true | Enables column sorting by clicking headers. |
| MultiSort | `bool` | true | Enables multi-column sorting via Ctrl/⌘+click with priority badges. |
| Filterable | `bool` | false | Renders a per-column quick-filter row. |
| FilterOperators | `bool` | false | Shows an operator dropdown next to text/number/date filter editors so users pick the comparison (contains/starts with/=/≠/>/≥/</≤) instead of the fixed default. |
| Strings | `BitDataGridStrings` | new() | All user-visible strings rendered by the grid; assign a customized instance to localize the UI. |
| Resizable | `bool` | false | Enables column resizing by dragging header edges. |
| Reorderable | `bool` | false | Enables column reordering via drag-and-drop (mouse via native HTML5 DnD, touch/pen via a pointer-event fallback). |
| Groupable | `bool` | false | Enables grouping via a header button on groupable columns. |
| ShowToolbar | `bool` | false | Renders the toolbar area. |
| ShowColumnChooser | `bool` | false | Renders a column show/hide chooser in the toolbar. |
| ShowCsvExport | `bool` | false | Renders a CSV export button. The export covers all matching rows in every data mode, not just the rendered ones. |
| ShowExcelExport | `bool` | false | Renders an Excel (.xlsx) export button. The workbook is generated in-process with no external dependency, covers all matching rows in every data mode, and mirrors the grid's layout: bold frozen header row, column widths, leading frozen columns as a freeze pane and ColSpan cells as merged cells. |
| ExcelExportStyled | `bool` | false | When true, Excel exports also carry the grid's current visual theme: the rendered header/row colors, striped alternating rows, border color and bold/italic fonts are sampled from the live DOM at export time (so the active theme - including dark mode - lands in the workbook). Falls back to the plain bold-header styling when JS is unavailable (prerendering). |
| CellNavigation | `bool` | false | Enables keyboard cell navigation with a roving tabindex (arrows/Home/End/PageUp/PageDown to move, Enter/F2 to edit, Delete to delete the focused row when Editable). |
| RowReorderable | `bool` | false | Enables drag-and-drop row reordering (mouse, touch and pen; plus keyboard via the drag handle's arrow keys). |
| OnRowReorder | `EventCallback<BitDataGridRowReorderEventArgs<TItem>>` | | Raised when a row is dropped onto another row during reordering. |
| SelectionMode | `BitDataGridSelectionMode` | BitDataGridSelectionMode.None | How rows can be selected (None/Single/Multiple). |
| SelectedItems | `IReadOnlyList<TItem>?` | null | The selected items (supports two-way binding). |
| SelectedItemsChanged | `EventCallback<IReadOnlyList<TItem>>` | | Raised when the selection changes. |
| OnRowClick | `EventCallback<TItem>` | | Raised when a row is clicked. |
| OnCellClick | `EventCallback<BitDataGridCellEventArgs<TItem>>` | | Raised when a data cell is clicked. |
| OnCellDoubleClick | `EventCallback<BitDataGridCellEventArgs<TItem>>` | | Raised when a data cell is double-clicked. |
| OnCellContextMenu | `EventCallback<BitDataGridCellEventArgs<TItem>>` | | Raised when a data cell is right-clicked. |
| IsRowSelectionDisabled | `Func<TItem, bool>?` | null | Predicate returning true when a given row may not be selected. |
| Pageable | `bool` | false | Enables paging with a pager UI. |
| PageSize | `int` | 20 | The number of rows per page. |
| PageSizeOptions | `int[]` | { 10, 20, 50, 100 } | The page-size options offered in the pager dropdown. |
| PagerPosition | `BitDataGridPagerPosition` | BitDataGridPagerPosition.Bottom | Where the pager renders relative to the grid. |
| Virtualize | `bool` | false | Renders only the visible rows for large datasets. Requires a fixed Height and RowHeight. In server mode (OnRead) with paging off, row windows are fetched on demand as the user scrolls; with OnLoadMore, the accumulated batches are virtualized so the DOM stays bounded. |
| RowHeight | `float` | 36 | Uniform row height in pixels (required when virtualizing). |
| RowHeightSelector | `Func<TItem, float>?` | null | Optional per-row height selector (ignored while virtualizing). |
| VirtualizeColumns | `bool` | false | Renders only the columns in (and near) the horizontal viewport, replacing scrolled-out runs with spacers - for grids with very many columns. Requires explicit px column widths; not applied with column header groups or ColSpans. |
| Editable | `bool` | false | Enables inline editing with a command column. |
| NewItemFactory | `Func<TItem>?` | null | Factory used by the toolbar Add button to create a new row. |
| OnRowSave | `EventCallback<TItem>` | | Raised when an edited row is saved. |
| OnRowCancel | `EventCallback<TItem>` | | Raised when an edit is cancelled. |
| OnRowDelete | `EventCallback<TItem>` | | Raised when a row is deleted (via the command column's Delete button or the Delete key in cell navigation). |
| OnRowCreate | `EventCallback<TItem>` | | Raised when a new row is created. |
| EmptyTemplate | `RenderFragment?` | null | Custom content rendered when there is no data. |
| ToolbarTemplate | `RenderFragment?` | null | Custom content rendered in the toolbar's start area. |
| DetailTemplate | `RenderFragment<TItem>?` | null | Expandable master-detail content rendered under a row. |
## Public Members
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| RefreshAsync | `Task` | | Recomputes the data view (filter → sort → group → page) and re-renders the grid. |
| SortByAsync | `Task` | | SortByAsync(columnId, direction, additive) - programmatically sorts by a column; BitDataGridSortDirection.None removes the sort. |
| ClearSortsAsync | `Task` | | Removes all active sorts and refreshes. |
| ApplyFilterAsync | `Task` | | ApplyFilterAsync(columnId, operator, value) - programmatically applies a filter, replacing any existing one on the column. |
| ClearFilterAsync | `Task` | | ClearFilterAsync(columnId) - removes the filter(s) applied to a column. |
| ClearFiltersAsync | `Task` | | Clears all active column filters and refreshes. |
| GroupByAsync | `Task` | | GroupByAsync(columnId) - adds the column as the next (nested) grouping level. |
| UngroupAsync | `Task` | | UngroupAsync(columnId) - removes the column's grouping level. |
| ClearGroupsAsync | `Task` | | Removes all active groupings and refreshes. |
| GoToPageAsync | `Task` | | GoToPageAsync(page) - navigates to the given 1-based page (clamped to the valid range). |
| SetPageSizeAsync | `Task` | | SetPageSizeAsync(size) - changes the page size and resets to the first page (without mutating the PageSize parameter). |
| GetState | `BitDataGridState` | | Captures the user-adjustable state (page, page size, sorts, filters, groups, column layout) as a serializable snapshot. |
| ApplyStateAsync | `Task` | | ApplyStateAsync(state) - restores a state snapshot captured by GetState. |
| ExportCsvAsync | `Task` | | Generates the full (filtered/sorted) data as CSV and triggers a client-side download. Covers all matching rows in every data mode - server/infinite modes fetch them through OnRead/OnLoadMore, tree mode includes collapsed branches. |
| ToCsv | `string` | | Builds a CSV string of the full dataset synchronously - tree mode includes collapsed branches, queryable mode covers all pages. Only server/infinite modes are limited to the loaded rows (their providers are async); use ToCsvAsync there. |
| ToCsvAsync | `Task<string>` | | Builds a CSV string of the full dataset - server/infinite modes issue an OnRead/OnLoadMore request with no paging (Take = null), tree mode includes collapsed branches. |
| ToExcelAsync | `Task<byte[]>` | | Generates the full (filtered/sorted) dataset as an Excel workbook (.xlsx), covering all matching rows in every data mode (like ToCsvAsync). The workbook mirrors the grid's layout: bold frozen header row, column widths, leading frozen columns as a freeze pane and ColSpan cells as merged cells; with ExcelExportStyled it also carries the grid's rendered theme (colors, striping, borders, fonts). |
| ExportExcelAsync | `Task` | | Generates the .xlsx workbook and triggers a client-side download. |
| ActiveSorts | `IReadOnlyList<BitDataGridSortDescriptor>` | [] | The active sort descriptors, in priority order. |
| ActiveFilters | `IReadOnlyList<BitDataGridFilterDescriptor>` | [] | The active filter descriptors. |
| ActiveGroups | `IReadOnlyList<BitDataGridGroupDescriptor>` | [] | The active group descriptors, in nesting order. |
| TotalCount | `int` | 0 | Total number of rows in the current (filtered) view; the server-reported total in server mode. |
| TotalPages | `int` | 1 | Total number of pages while paging is active. |
| CurrentPage | `int` | 1 | The 1-based current page. |
| ExpandAllAsync | `Task` | | Expands every node in the tree. No-op outside tree mode. |
| CollapseAllAsync | `Task` | | Collapses every node in the tree. No-op outside tree mode. |
## Enums
### BitDataGridColumnAlign Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Left | 0 | |
| Center | 1 | |
| Right | 2 | |
### BitDataGridSortDirection Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| None | 0 | |
| Ascending | 1 | |
| Descending | 2 | |
### BitDataGridSelectionMode Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| None | 0 | |
| Single | 1 | |
| Multiple | 2 | |
### BitDataGridAggregateType Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| None | 0 | |
| Sum | 1 | |
| Average | 2 | |
| Count | 3 | |
| Min | 4 | |
| Max | 5 | |
| Custom | 6 | The value was produced by the column's custom AggregateBy delegate rather than a built-in function. |
### BitDataGridPagerPosition Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Bottom | 0 | |
| Top | 1 | |
| TopAndBottom | 2 | |
### BitDir Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Ltr | 0 | |
| Rtl | 1 | |
| Auto | 2 | |
### BitDataGridColumnDataType Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Auto | 0 | |
| Text | 1 | |
| Number | 2 | |
| Boolean | 3 | |
| Date | 4 | |
| DateTime | 5 | |
| DateTimeOffset | 6 | |
| Enum | 7 | |
### BitDataGridFilterOperator Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Unspecified | 0 | |
| Contains | 1 | |
| DoesNotContain | 2 | |
| StartsWith | 3 | |
| EndsWith | 4 | |
| Equals | 5 | |
| NotEquals | 6 | |
| GreaterThan | 7 | |
| GreaterThanOrEqual | 8 | |
| LessThan | 9 | |
| LessThanOrEqual | 10 | |
| IsEmpty | 11 | |
| IsNotEmpty | 12 | |
## Sub Classes
### BitDataGridColumn Properties
Defines a column inside a BitDataGrid. Place these as child content of the grid.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Field | `string?` | null | Name of the property this column is bound to. Supports nested paths ("Address.City"). Prefer Property for a strongly typed, refactor-safe alternative. |
| Property | `Expression<Func<TItem, object?>>?` | null | Typed selector of the property this column is bound to, e.g. Property="p => p.Name". A strongly typed, refactor-safe alternative to Field that supports nested member chains (p => p.Address.City). Takes precedence over Field when both are set. |
| ColumnId | `string?` | null | Stable identifier for the column. Defaults to the resolved Property/Field path. |
| Title | `string?` | null | Header text. Defaults to a humanized Property/Field name. |
| Width | `string?` | null | CSS width, e.g. "120px" or "20%". When null the column shares remaining space. |
| MinWidth | `int` | 60 | Minimum width in pixels the column can be resized to. |
| MaxWidth | `int?` | null | Maximum width in pixels the column can be resized to. |
| Sortable | `bool?` | null | Overrides the grid-level Sortable for this column. |
| SortBy | `Func<TItem, object?>?` | null | Custom sort key selector. Enables sorting for template-only columns and overrides the field value as the sort key (client mode). |
| SortDescendingFirst | `bool` | false | When true, the first click on the header sorts descending instead of ascending. |
| Validate | `Func<TItem, object?, string?>?` | null | Validator for inline edits: receives the row and the proposed value, returns an error message to reject it (blocking Save) or null to accept. |
| Filterable | `bool?` | null | Overrides the grid-level Filterable for this column. |
| FilterOperators | `bool?` | null | Overrides the grid-level FilterOperators (the operator dropdown next to this column's filter editor). |
| Resizable | `bool?` | null | Overrides the grid-level Resizable for this column. |
| Reorderable | `bool?` | null | Overrides the grid-level Reorderable for this column. |
| Editable | `bool?` | null | Overrides the grid-level Editable for this column. |
| Groupable | `bool?` | null | Overrides the grid-level Groupable for this column. |
| Frozen | `bool` | false | Pins the column to the start edge so it stays visible while scrolling horizontally. |
| FrozenEnd | `bool` | false | Pins the column to the end edge (right in LTR, left in RTL). Typical for action/status columns. |
| Group | `string?` | null | Optional header group name. Consecutive columns sharing the same value render under a single spanning header cell. |
| ColSpan | `Func<TItem, int?>?` | null | Optional per-row column span. |
| Visible | `bool` | true | Whether the column is visible. |
| Align | `BitDataGridColumnAlign` | BitDataGridColumnAlign.Left | Horizontal alignment of cell content. |
| Format | `string?` | null | A .NET format string applied to the value (e.g. "C2", "yyyy-MM-dd"). |
| DataType | `BitDataGridColumnDataType` | BitDataGridColumnDataType.Auto | The data type used to pick the editor/filter. |
| Aggregate | `BitDataGridAggregateType` | BitDataGridAggregateType.None | The footer/group aggregate function. |
| AggregateBy | `Func<IReadOnlyList<TItem>, object?>?` | null | Custom aggregate function for computations beyond the built-ins (e.g. distinct count). Takes precedence over Aggregate. |
| AggregateFormat | `string?` | null | Format string for the aggregate value. Falls back to Format. |
| HeaderClass | `string?` | null | Custom CSS class applied to the header cell. |
| CellClass | `string?` | null | Custom CSS class applied to each data cell. |
| Template | `RenderFragment<TItem>?` | null | Custom rendering for a data cell. |
| HeaderTemplate | `RenderFragment?` | null | Custom rendering for the header cell content. |
| EditTemplate | `RenderFragment<TItem>?` | null | Custom editor rendered when the row/cell is in edit mode. |
| FooterTemplate | `RenderFragment<BitDataGridAggregateResult>?` | null | Custom rendering for the footer/aggregate cell. |
### BitDataGridReadRequest Properties
Describes the data the grid needs from a server-side/infinite source (passed to OnRead/OnLoadMore).
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Skip | `int` | 0 | Zero-based number of items to skip. |
| Take | `int?` | null | Maximum number of items to return (null means all). |
| Sorts | `IReadOnlyList<BitDataGridSortDescriptor>` | [] | The active sort descriptors ordered by priority. |
| Filters | `IReadOnlyList<BitDataGridFilterDescriptor>` | [] | The active filter descriptors. |
| Groups | `IReadOnlyList<BitDataGridGroupDescriptor>` | [] | The active group descriptors in nesting order, letting a server-side handler reconstruct the grouping. Empty when no grouping is active. |
| CancellationToken | `CancellationToken` | | A token that is cancelled when the request is superseded by a newer one. |
### BitDataGridReadResult<TItem> Properties
Result returned from a grid's OnRead/OnLoadMore callback.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Items | `IReadOnlyList<TItem>` | | The items for the current page/window. |
| TotalCount | `int` | | The total number of items matching the current filters (ignored in infinite mode). |
| Aggregates | `IReadOnlyList<BitDataGridAggregateResult>?` | null | Optional aggregates computed by the data source over the whole filtered dataset; when provided, the footer shows these instead of aggregating the current page locally. |
### BitDataGridCellEventArgs<TItem> Properties
Arguments passed to cell-level event callbacks.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Item | `TItem` | | The row item. |
| Column | `BitDataGridColumn<TItem>` | | The column the cell belongs to. |
| ColumnId | `string` | | The column field/identifier. |
| ColumnTitle | `string` | | The column's display title. |
| Value | `object?` | null | The raw value of the cell. |
| Mouse | `MouseEventArgs` | | The underlying browser mouse event. |
### BitDataGridRowReorderEventArgs<TItem> Properties
Arguments raised when a row is reordered via drag-and-drop.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| DraggedItem | `TItem` | | The dragged row item. |
| TargetItem | `TItem` | | The drop-target row item. |
| FromIndex | `int?` | | The original index of the dragged item, or null when the bound Items is not an indexable list. |
| ToIndex | `int?` | | The destination index, or null when the bound Items is not an indexable list. |
### BitDataGridSortDescriptor Properties
Describes the sort state applied to a single column (found on BitDataGridReadRequest.Sorts).
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ColumnId | `string` | | The identifier of the column being sorted. |
| Direction | `BitDataGridSortDirection` | BitDataGridSortDirection.Ascending | The sort direction. |
| Priority | `int` | int.MaxValue | Priority for multi-column sorting (1 = primary). |
### BitDataGridFilterDescriptor Properties
Describes a filter applied to a single column (found on BitDataGridReadRequest.Filters).
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ColumnId | `string` | | The identifier of the column being filtered. |
| Operator | `BitDataGridFilterOperator` | BitDataGridFilterOperator.Contains | The comparison operator applied to the value. |
| Value | `object?` | null | The value compared against the column's cell value. |
### BitDataGridGroupDescriptor Properties
Describes a grouping applied to a column.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ColumnId | `string` | | The identifier of the column being grouped. |
| Direction | `BitDataGridSortDirection` | BitDataGridSortDirection.Ascending | The sort direction applied to the group keys. |
### BitDataGridAggregateResult Properties
Holds the computed aggregate value for a column footer or group (passed to a column's FooterTemplate).
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ColumnId | `string` | | The identifier of the aggregated column. |
| Type | `BitDataGridAggregateType` | | The aggregate function that produced the value. |
| Value | `object?` | null | The raw aggregate value. |
| FormattedValue | `string` | string.Empty | The aggregate value formatted using the column's AggregateFormat/Format. |
### BitDataGridState Properties
A serializable snapshot of the grid's user-adjustable state, captured with GetState() and restored with ApplyStateAsync(). Enables persisting grid state across sessions.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| CurrentPage | `int` | 1 | The 1-based current page. |
| PageSize | `int?` | null | The user-selected page size, or null when the grid's PageSize parameter applies. |
| Sorts | `List<BitDataGridSortDescriptor>` | [] | The active sort descriptors. |
| Filters | `List<BitDataGridFilterDescriptor>` | [] | The active filter descriptors. |
| Groups | `List<BitDataGridGroupDescriptor>` | [] | The active group descriptors. |
| Columns | `List<BitDataGridColumnState>` | [] | Per-column layout state. |
### BitDataGridColumnState Properties
A per-column layout entry inside BitDataGridState.Columns.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ColumnId | `string` | string.Empty | The column's stable identifier (ColumnId or Field). |
| Visible | `bool` | true | Whether the column is shown (column-chooser state). |
| Width | `double?` | null | The resized width in pixels, or null when the column was never resized. |
| Order | `int` | 0 | The display position among all columns. |
### BitDataGridStrings Properties
All user-visible (and screen-reader) strings rendered by the grid, defaulting to English. Assign a customized instance to the Strings parameter to localize - including empty/loading texts, pager texts, filter placeholders and operators, edit buttons, aggregate labels, aria-labels and live-region announcements.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| EmptyText | `string` | "No records to display." | Shown when the grid has no rows to display. |
| LoadingText | `string` | "Loading…" | Shown while Loading is true. |
| PagerRangeFormat | `string` | "{0}–{1} of {2}" | Pager range summary format. |
| PagerPageFormat | `string` | "Page {0} of {1}" | Pager page summary format. |
| InvalidValueError | `string` | "Invalid value for {0}." | Error shown when an edited value can't be converted to the column's type. |
| … | `string` | | Plus ~40 more: toolbar/edit button texts, filter placeholder and operator labels, boolean/enum option texts, group/detail/tree/reorder aria-labels, aggregate label formats and aria-live announcements. |
## Examples
\n**Basic & sorting**:
```razor
@* TItem is inferred from the grid for all columns; Property is the strongly typed
alternative to Field="Name"; the Columns wrapper element is an optional alias
of the grid's child content. *@
```
```csharp
private List products = SampleData.Generate(50);
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Filtering & paging**:
```razor
```
```csharp
private List products = SampleData.Generate(200);
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Selection**:
```razor
```
```csharp
private List products = SampleData.Generate(60);
private IReadOnlyList selected = new List();
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Inline editing**:
```razor
@* TItem stays explicit here: EventCallback parameters (OnRowSave/OnRowDelete/OnRowCreate)
bound to method groups cannot participate in generic type inference. *@
```
```csharp
private List products = SampleData.Generate(25);
private int nextId;
protected override void OnInitialized() => nextId = products.Max(p => p.Id) + 1;
private Product CreateProduct() => new()
{
Id = nextId++,
Name = "New product",
Category = Category.Electronics,
ReleaseDate = DateTime.Today
};
private void OnCreate(Product p) { /* called when a new row starts being added */ }
private void OnSave(Product p)
{
// The grid is keyed by Id (KeyField="p => p.Id") and hands back an edited copy, so match on Id
// rather than the object reference: update the existing row in place, or insert a brand-new one.
var index = products.FindIndex(x => x.Id == p.Id);
if (index >= 0) products[index] = p;
else products.Insert(0, p);
}
private void OnDelete(Product p) => products.RemoveAll(x => x.Id == p.Id);
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Grouping & aggregates**:
```razor
```
```csharp
private List products = SampleData.Generate(80);
// A custom aggregate: receives the rows of the footer's view (or of each group) and returns any value.
private object? DistinctSuppliers(IReadOnlyList rows)
=> $"{rows.Select(p => p.Supplier).Distinct().Count()} distinct";
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Templates & detail rows**:
```razor
📦 Product
Total: @agg.FormattedValue
@p.Stock in stock
@* A template-only column (no Field) becomes sortable through its SortBy key selector. *@
@((p.Price * p.Stock).ToString("C0"))
```
```csharp
private List products = SampleData.Generate(30);
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Resize, reorder & freeze columns**:
```razor
```
```csharp
private List products = SampleData.Generate(40);
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Column header groups**:
```razor
```
```csharp
private List products = SampleData.Generate(40);
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Column spanning**:
```razor
@p.Name
```
```csharp
private List products = SampleData.Generate(40);
private int? NameSpan(Product p) => p.Discontinued ? 2 : null;
private int? PriceSpan(Product p) => p.Price > 800 ? 2 : null;
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Virtualization**:
```razor
```
```csharp
private List products = SampleData.Generate(10_000);
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Server-side data**:
```razor
```
```csharp
private bool loading;
private readonly List all = SampleData.Generate(523);
private async Task> LoadData(BitDataGridReadRequest request)
{
loading = true;
await InvokeAsync(StateHasChanged); // re-render so the loading indicator shows
try
{
await Task.Delay(250, request.CancellationToken); // simulate a backend round-trip
IEnumerable query = all;
// filtering - honor the operator the grid emits, not just contains/equals
foreach (var f in request.Filters)
{
query = f.ColumnId switch
{
// text column uses the string operators from the grid's text filter editor
nameof(Product.Name) => query.Where(p => MatchText(p.Name, f)),
// numeric columns receive a typed value with a comparison operator, so honor the
// requested equality/range operator instead of a hard-coded equals
nameof(Product.Price) => query.Where(p => MatchComparable(p.Price, f)),
nameof(Product.Id) => query.Where(p => MatchComparable(p.Id, f)),
_ => query
};
}
// sorting (honor every active sort descriptor, not just the first)
IOrderedEnumerable? ordered = null;
foreach (var sort in request.Sorts)
{
Func key = sort.ColumnId switch
{
nameof(Product.Name) => p => p.Name,
nameof(Product.Price) => p => p.Price,
_ => p => p.Id
};
if (ordered is null)
ordered = sort.Direction == BitDataGridSortDirection.Descending
? query.OrderByDescending(key)
: query.OrderBy(key);
else
ordered = sort.Direction == BitDataGridSortDirection.Descending
? ordered.ThenByDescending(key)
: ordered.ThenBy(key);
}
if (ordered is not null) query = ordered;
// paging
var filtered = query.ToList();
var items = filtered.Skip(request.Skip).Take(request.Take ?? filtered.Count).ToList();
// A superseded request can finish filtering/sorting/paging after a newer one started; bail
// out before returning so the grid never receives stale rows for a cancelled load.
request.CancellationToken.ThrowIfCancellationRequested();
return new BitDataGridReadResult(items, filtered.Count);
}
finally
{
// Only the active request should clear the loading state; a superseded request observes a
// cancelled token, so skip the reset and let the newer in-flight load own the indicator.
if (!request.CancellationToken.IsCancellationRequested)
{
loading = false;
await InvokeAsync(StateHasChanged); // re-render after the load completes (runs as a callback)
}
}
}
// Applies a text-column filter the way the grid's text editor emits it.
private static bool MatchText(string value, BitDataGridFilterDescriptor f)
{
if (f.Operator is BitDataGridFilterOperator.IsEmpty) return string.IsNullOrEmpty(value);
if (f.Operator is BitDataGridFilterOperator.IsNotEmpty) return !string.IsNullOrEmpty(value);
var term = f.Value?.ToString();
if (string.IsNullOrWhiteSpace(term)) return true;
return f.Operator switch
{
BitDataGridFilterOperator.Contains => value.Contains(term, StringComparison.OrdinalIgnoreCase),
BitDataGridFilterOperator.DoesNotContain => !value.Contains(term, StringComparison.OrdinalIgnoreCase),
BitDataGridFilterOperator.StartsWith => value.StartsWith(term, StringComparison.OrdinalIgnoreCase),
BitDataGridFilterOperator.EndsWith => value.EndsWith(term, StringComparison.OrdinalIgnoreCase),
BitDataGridFilterOperator.Equals => string.Equals(value, term, StringComparison.OrdinalIgnoreCase),
BitDataGridFilterOperator.NotEquals => !string.Equals(value, term, StringComparison.OrdinalIgnoreCase),
_ => true
};
}
// Applies a non-text-column filter against the typed value the grid emits so the requested
// equality/range operator is honored instead of a substring match on ToString().
private static bool MatchComparable(T value, BitDataGridFilterDescriptor f) where T : IComparable
{
if (f.Operator is BitDataGridFilterOperator.IsEmpty) return value is null;
if (f.Operator is BitDataGridFilterOperator.IsNotEmpty) return value is not null;
if (f.Value is null) return true;
if (f.Value is not T typed) return true;
var cmp = value.CompareTo(typed);
return f.Operator switch
{
BitDataGridFilterOperator.Equals => cmp == 0,
BitDataGridFilterOperator.NotEquals => cmp != 0,
BitDataGridFilterOperator.GreaterThan => cmp > 0,
BitDataGridFilterOperator.GreaterThanOrEqual => cmp >= 0,
BitDataGridFilterOperator.LessThan => cmp < 0,
BitDataGridFilterOperator.LessThanOrEqual => cmp <= 0,
_ => true
};
}
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Infinite scrolling**:
```razor
```
```csharp
private readonly List all = SampleData.Generate(2_017);
private async Task> LoadMore(BitDataGridReadRequest request)
{
await Task.Delay(350, request.CancellationToken); // simulate a backend round-trip
IEnumerable query = all;
// Apply every active sort descriptor before paging out the batch.
IOrderedEnumerable? ordered = null;
foreach (var sort in request.Sorts)
{
Func key = sort.ColumnId switch
{
nameof(Product.Name) => p => p.Name,
nameof(Product.Price) => p => p.Price,
_ => p => p.Id
};
if (ordered is null)
ordered = sort.Direction == BitDataGridSortDirection.Descending
? query.OrderByDescending(key)
: query.OrderBy(key);
else
ordered = sort.Direction == BitDataGridSortDirection.Descending
? ordered.ThenByDescending(key)
: ordered.ThenBy(key);
}
if (ordered is not null) query = ordered;
// Take is the batch size while scrolling; null means "all rows" (issued by CSV/Excel exports).
var batch = query.Skip(request.Skip).Take(request.Take ?? all.Count).ToList();
// Drop a superseded batch before returning so a cancelled load never yields stale rows.
request.CancellationToken.ThrowIfCancellationRequested();
// Pass 0 as the total count to signal there is no known total (infinite scrolling).
return new BitDataGridReadResult(batch, 0);
}
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Tree view (hierarchical rows)**:
```razor
```
```csharp
private List roots = FileSystemData.Build();
private BitDataGrid? grid;
private async Task ExpandAll() { if (grid is not null) await grid.ExpandAllAsync(); }
private async Task CollapseAll() { if (grid is not null) await grid.CollapseAllAsync(); }
public class FileNode
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Kind { get; set; } = "Folder";
public long Size { get; set; }
public DateTime Modified { get; set; }
public List Children { get; set; } = new();
}
public static class FileSystemData
{
public static List Build()
{
var id = 0;
var baseDate = new DateTime(2025, 1, 1);
FileNode Folder(string name, params FileNode[] children)
{
var node = new FileNode { Id = ++id, Name = name, Kind = "Folder", Modified = baseDate.AddDays(id), Children = children.ToList() };
node.Size = node.Children.Sum(c => c.Size);
return node;
}
FileNode File(string name, long size) => new() { Id = ++id, Name = name, Kind = "File", Size = size, Modified = baseDate.AddDays(id) };
return new List
{
Folder("src",
Folder("BitDataGrid",
File("BitDataGrid.razor", 24_500),
File("BitDataGrid.razor.cs", 41_200),
Folder("Models",
File("BitDataGridColumnAlign.cs", 320),
File("BitDataGridSortDescriptor.cs", 540),
File("BitDataGridFilterOperator.cs", 610)),
Folder("Infrastructure",
File("BitDataGridDataProcessor.cs", 8_900),
File("BitDataGridPropertyAccessor.cs", 3_400))),
Folder("BitDataGrid.Demo",
File("Program.cs", 1_200),
Folder("Components",
File("App.razor", 760),
File("Routes.razor", 280)))),
Folder("docs",
File("README.md", 6_400),
File("CHANGELOG.md", 2_100)),
Folder("assets",
File("logo.svg", 4_800),
File("styles.css", 12_300),
File("favicon.ico", 1_150)),
File("LICENSE", 1_070),
File(".gitignore", 410)
};
}
}
```
\n**Master detail**:
```razor
```
```csharp
private List suppliers = BuildSuppliers();
private static List BuildSuppliers() =>
SampleData.Generate(240)
.GroupBy(p => p.Supplier)
.Select(g => new SupplierModel { Name = g.Key, Products = g.OrderBy(p => p.Name).ToList() })
.OrderBy(s => s.Name)
.ToList();
public sealed class SupplierModel
{
public string Name { get; set; } = "";
public List Products { get; set; } = new();
public int ProductCount => Products.Count;
public int TotalStock => Products.Sum(p => p.Stock);
public decimal AveragePrice => Products.Count == 0 ? 0 : Math.Round(Products.Average(p => p.Price), 2);
}
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Row reordering**:
```razor
```
```csharp
private List products = SampleData.Generate(12);
private void OnReorder(BitDataGridRowReorderEventArgs e)
{
// e.DraggedItem, e.TargetItem, e.FromIndex, e.ToIndex
// FromIndex/ToIndex are int? and may be null when Items is not an indexable IList.
}
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Cell events & context menu**:
```razor
@if (cellMenuArgs is not null)
{
}
```
```csharp
private List products = SampleData.Generate(40);
private BitDataGrid? grid;
private BitDataGridCellEventArgs? cellMenuArgs;
private int cellMenuX;
private int cellMenuY;
private ElementReference cellMenuFirstItem;
private bool cellMenuFocusPending;
private void OnCellClick(BitDataGridCellEventArgs e) { /* e.Item, e.ColumnTitle, e.Value */ }
private void OnCellDoubleClick(BitDataGridCellEventArgs e) { /* e.Item, e.ColumnTitle, e.Value */ }
private void OnCellContextMenu(BitDataGridCellEventArgs e)
{
cellMenuArgs = e;
// ClientX/Y are viewport coordinates, matching the menu's position:fixed placement.
cellMenuX = (int)e.Mouse.ClientX;
cellMenuY = (int)e.Mouse.ClientY;
// Move focus into the menu once it renders so keyboard users can operate/dismiss it.
cellMenuFocusPending = true;
}
private void CloseCellMenu() => cellMenuArgs = null;
private void OnCellMenuKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Escape") CloseCellMenu();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (cellMenuFocusPending && cellMenuArgs is not null)
{
cellMenuFocusPending = false;
await cellMenuFirstItem.FocusAsync();
}
}
private async Task CopyCellValue()
{
if (cellMenuArgs is null) return;
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", cellMenuArgs.Value?.ToString() ?? "");
cellMenuArgs = null;
}
private async Task DeleteCellMenuRow()
{
if (cellMenuArgs is null) return;
products.Remove(cellMenuArgs.Item);
cellMenuArgs = null;
// The grid caches its processed view; mutating the bound list in place requires an explicit refresh.
if (grid is not null) await grid.RefreshAsync();
}
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Keyboard cell navigation**:
```razor
```
```csharp
private List products = SampleData.Generate(40);
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Filter operators**:
```razor
@* A column can override the grid-level setting and keep the fixed default filter. *@
```
```csharp
private List products = SampleData.Generate(150);
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Edit validation**:
```razor
```
```csharp
private List products = SampleData.Generate(30);
// Validators receive the row and the proposed (type-converted) value.
// Return an error message to reject the edit (blocking Save), or null to accept.
private string? ValidateName(Product product, object? value)
=> string.IsNullOrWhiteSpace(value as string) ? "Name is required." : null;
private string? ValidatePrice(Product product, object? value)
=> value is decimal price && price < 0 ? "Price cannot be negative." : null;
private string? ValidateStock(Product product, object? value)
=> value is int stock && stock < 0 ? "Stock cannot be negative." : null;
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**State persistence**:
```razor
Save state
Restore state
@stateStatus
```
```csharp
private List products = SampleData.Generate(120);
private BitDataGrid? grid;
private BitDataGridState? savedState;
private string stateStatus = "Adjust the grid, then save its state.";
private void SaveGridState()
{
// The snapshot is serializable - persist it to local storage or a user-preferences store.
savedState = grid?.GetState();
stateStatus = savedState is null
? "Nothing to save yet."
: $"Saved: page {savedState.CurrentPage}, {savedState.Sorts.Count} sort(s), {savedState.Filters.Count} filter(s).";
}
private async Task RestoreGridState()
{
if (grid is null || savedState is null) return;
await grid.ApplyStateAsync(savedState);
stateStatus = "State restored.";
}
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Server-side virtualization**:
```razor
```
```csharp
private List all = SampleData.Generate(100_000);
private async Task> LoadVirtualServerData(BitDataGridReadRequest request)
{
// Simulate backend latency. Superseded scroll windows are cancelled by the grid; let the
// OperationCanceledException propagate so the grid discards the stale read - returning an
// empty result instead would be rendered as real data and blank the viewport.
await Task.Delay(150, request.CancellationToken);
IEnumerable query = all;
// ...apply request.Filters and request.Sorts (see the Server-side data example)...
var filtered = query.ToList();
var items = filtered.Skip(request.Skip).Take(request.Take ?? filtered.Count).ToList();
// Aggregates computed over the WHOLE filtered dataset (not just the returned window),
// so the footer shows a real grand total instead of a per-window number.
var priceSum = filtered.Sum(p => p.Price);
var aggregates = new List
{
new()
{
ColumnId = nameof(Product.Price),
Type = BitDataGridAggregateType.Sum,
Value = priceSum,
FormattedValue = priceSum.ToString("C0")
}
};
return new BitDataGridReadResult(items, filtered.Count) { Aggregates = aggregates };
}
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Localization**:
```razor
```
```csharp
private List products = SampleData.GeneratePersian(60);
// Every user-visible string has an English default; override what you need.
private readonly BitDataGridStrings persianStrings = new()
{
EmptyText = "رکوردی برای نمایش وجود ندارد.",
LoadingText = "در حال بارگذاری…",
FilterPlaceholder = "فیلتر…",
FilterAllText = "همه",
PagerRangeFormat = "{0}–{1} از {2}",
PagerPageFormat = "صفحهٔ {0} از {1}",
PerPageFormat = "{0} در صفحه",
RowsPerPageLabel = "تعداد ردیف در صفحه",
FirstPageLabel = "صفحهٔ اول",
PreviousPageLabel = "صفحهٔ قبل",
NextPageLabel = "صفحهٔ بعد",
LastPageLabel = "صفحهٔ آخر",
ClearFiltersText = "حذف فیلترها",
};
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator that produces Persian sample data for the RTL demo.
public static class SampleData
{
static readonly string[] Adjectives =
{ "فوقالعاده", "ممتاز", "اقتصادی", "هوشمند", "کلاسیک", "حرفهای", "کوچک", "بزرگ", "قدیمی", "مدرن", "لوکس", "فشرده" };
static readonly string[] Nouns =
{ "ویجت", "گجت", "بلندگو", "دفترچه", "ژاکت", "چراغ", "مخلوطکن", "پهپاد", "کولهپشتی", "کفش", "دوربین", "لیوان" };
static readonly string[] Suppliers =
{ "شرکت آلفا", "گلوبکس", "اینیتک", "آمبرلا", "سویلنت", "صنایع استارک", "شرکت وین", "ونکا" };
public static List GeneratePersian(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**IQueryable data source**:
```razor
```
```csharp
// Any IQueryable works - filtering, sorting and paging are composed as expression trees the
// provider executes at the source, so with EF Core this becomes SQL WHERE/ORDER BY/OFFSET
// and only the current page is materialized:
// private IQueryable products => dbContext.Products;
private IQueryable products = SampleData.Generate(400).AsQueryable();
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Excel export**:
```razor
@* ExcelExportStyled samples the grid's rendered theme (colors, striping, borders, fonts)
from the live DOM at export time and bakes it into the workbook's styles. *@
```
```csharp
private List products = SampleData.Generate(120);
// Programmatic exports are also available:
// string csv = await grid.ToCsvAsync(); // all matching rows in every data mode
// byte[] xlsx = await grid.ToExcelAsync(); // real .xlsx workbook, no external library
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
\n**Programmatic control**:
```razor
Sort by price ↓
Filter price > 500
Group by category
Go to page 3
```
```csharp
private List products = SampleData.Generate(200);
private BitDataGrid? grid;
private async Task SortByPrice() => await grid!.SortByAsync(nameof(Product.Price), BitDataGridSortDirection.Descending);
private async Task FilterExpensive() => await grid!.ApplyFilterAsync(nameof(Product.Price), BitDataGridFilterOperator.GreaterThan, 500m);
private async Task GroupByCategory() => await grid!.GroupByAsync(nameof(Product.Category));
private async Task GoToPage3() => await grid!.GoToPageAsync(3);
// Also available: ClearSortsAsync, ClearFilterAsync/ClearFiltersAsync, UngroupAsync/ClearGroupsAsync,
// SetPageSizeAsync, RefreshAsync, GetState/ApplyStateAsync, ToCsvAsync/ToExcelAsync.
public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery }
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Category Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public double Rating { get; set; }
public bool Discontinued { get; set; }
public DateTime ReleaseDate { get; set; }
public string Supplier { get; set; } = "";
}
// Deterministic generator so the demo data is reproducible.
public static class SampleData
{
static readonly string[] Adjectives =
{ "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" };
static readonly string[] Nouns =
{ "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" };
static readonly string[] Suppliers =
{ "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" };
public static List Generate(int count, int seed = 42)
{
var rng = new Random(seed);
var categories = Enum.GetValues();
var list = new List(count);
var referenceDate = new DateTime(2024, 1, 1);
for (int i = 1; i <= count; i++)
{
list.Add(new Product
{
Id = i,
Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}",
Category = categories[rng.Next(categories.Length)],
Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2),
Stock = rng.Next(0, 500),
Rating = Math.Round(rng.NextDouble() * 4 + 1, 1),
Discontinued = rng.Next(0, 5) == 0,
ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)),
Supplier = Suppliers[rng.Next(Suppliers.Length)]
});
}
return list;
}
}
```
Supplier: @p.Supplier