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
Supplier: @p.Supplier
📦 Product Total: @agg.FormattedValue @* A template-only column (no Field) becomes sortable through its SortBy key selector. *@
``` ```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 ``` ```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; } } ```