DataGridControl renders any collection as a tabular layout with sortable, resizable columns. It supports pagination, virtual scrolling for large datasets, custom cell templates, and column-level formatting — all wired up with a fluent builder API.
DataGridControl anatomy: column types and rendering mode options.
Basic Usage
The minimum setup is a DataGridControl(data) call plus one or more column definitions.
record Product(string Name, decimal Price, int Stock);
var products = new[]
{
new Product("Widget", 9.99m, 100),
new Product("Gadget", 24.99m, 50),
new Product("Gizmo", 14.99m, 75)
};
new DataGridControl(products)
.WithColumn(new PropertyColumnControl<string> { Property = "name" }.WithTitle("Product Name"))
.WithColumn(new PropertyColumnControl<decimal> { Property = "price" }.WithTitle("Price"))
.WithColumn(new PropertyColumnControl<int> { Property = "stock" }.WithTitle("In Stock"))
Property names are camelCase. The
Propertyvalue onPropertyColumnControlmust match the camelCase form of the record/class property name (e.g."name"forName,"unitPrice"forUnitPrice).
Column Types
PropertyColumnControl
Renders the value of a typed property from each row. The generic type parameter controls sorting and formatting behaviour.
new PropertyColumnControl<string> { Property = "email" }
.WithTitle("Email Address")
.WithWidth("200px")
.WithSortable(true) // default: true
.WithResizable(true) // default: true
.WithAlign("end") // start | center | end
.WithDefaultSort() // make this the initial sort column
.WithFormat("C2") // standard .NET format string (numbers, dates)
TemplateColumnControl
Places an arbitrary UiControl in every cell of the column. Use this for action buttons, badges, or any custom rendering.
new TemplateColumnControl(Controls.Button("View"))
.WithTitle("Actions")
.WithSortable(false) // action columns are rarely sortable
.WithWidth("100px")
Configuration Reference
DataGrid options
| Method | Purpose | Default |
|---|---|---|
WithVirtualize(bool) |
Enable virtual (windowed) scrolling | false |
WithItemSize(int) |
Row height in pixels (used by virtualizer) | 50 |
Resizable(bool) |
Allow column resizing globally | true |
WithPagination(bool) |
Enable built-in pagination | false |
WithItemsPerPage(int) |
Rows per page | — |
WithPageSizeOptions(int[]) |
Available page size choices | [5,10,25,50,100] |
WithShowHover(bool) |
Highlight row under the pointer | true |
WithSelectionMode(string) |
Row selection mode | — |
WithRowSelection(dataId, keyProperty, disabledReasonProperty?) |
Data-bound multi-row selection column — see "Row selection" below | — |
WithEmptyContent(control) |
Content shown when the dataset is empty | — |
WithLoading(bool) |
Show loading skeleton | false |
WithGenerateHeader(string) |
Header generation strategy ("Sticky", etc.) |
"Sticky" |
Column options (all column types)
| Method | Purpose | Default |
|---|---|---|
WithTitle(string) |
Column header text | — |
WithWidth(string) |
Fixed CSS width | — |
WithMinWidth(string) |
Minimum CSS width | — |
WithMaxWidth(string) |
Maximum CSS width | — |
WithAlign(string) |
Cell alignment (start, center, end) |
— |
WithSortable(bool) |
Enable column sorting | true |
WithResizable(bool) |
Enable column resizing | true |
WithVisible(bool) |
Show or hide the column | true |
WithFrozen(bool) |
Freeze column (pin to left edge) | false |
WithFilterable(bool) |
Enable column filtering | — |
PropertyColumnControl extras
| Method | Purpose |
|---|---|
WithFormat(string) |
.NET format string ("C2", "d", etc.) |
WithDefaultSort() |
Make this the initial sort column |
WithInitialSortDirection(dir) |
"Ascending" or "Descending" |
WithEditable() |
Allow inline editing |
WithPlaceholderText(string) |
Placeholder shown for null/empty cells |
WithNullDisplayText(string) |
Text rendered when value is null |
Common Patterns
Pagination for longer lists
record Item(int Id, string Name, string Category);
var items = Enumerable.Range(1, 25)
.Select(i => new Item(i, $"Item {i}", i % 2 == 0 ? "A" : "B"))
.ToArray();
new DataGridControl(items)
.WithPagination(true)
.WithItemsPerPage(5)
.WithColumn(new PropertyColumnControl<int> { Property = "id" }.WithTitle("ID"))
.WithColumn(new PropertyColumnControl<string> { Property = "name" }.WithTitle("Name"))
.WithColumn(new PropertyColumnControl<string> { Property = "category" }.WithTitle("Category"))
Action buttons
Use TemplateColumnControl for per-row commands. Disable sorting on it so users aren't confused by clicking the header.
record User(string Name, string Email);
var users = new[]
{
new User("Alice", "alice@example.com"),
new User("Bob", "bob@example.com"),
new User("Carol", "carol@example.com")
};
var actionButtons = Controls.Stack
.WithOrientation(Orientation.Horizontal)
.WithHorizontalGap("4px")
.WithView(Controls.Button("Edit"))
.WithView(Controls.Button("Delete"));
new DataGridControl(users)
.WithColumn(new PropertyColumnControl<string> { Property = "name" }.WithTitle("Name"))
.WithColumn(new PropertyColumnControl<string> { Property = "email" }.WithTitle("Email"))
.WithColumn(new TemplateColumnControl(actionButtons)
.WithTitle("Actions").WithSortable(false))
Virtual scrolling for large datasets
Enable WithVirtualize(true) together with a fixed WithItemSize so the renderer can calculate offsets without measuring every row.
record DataRow(int Id, string Value);
var largeDataset = Enumerable.Range(1, 100)
.Select(i => new DataRow(i, $"Row {i}"))
.ToArray();
new DataGridControl(largeDataset)
.WithVirtualize(true)
.WithItemSize(40)
.WithColumn(new PropertyColumnControl<int> { Property = "id" }.WithTitle("ID"))
.WithColumn(new PropertyColumnControl<string> { Property = "value" }.WithTitle("Value"))
Read-only report grid
For display-only tables, disable resizing and right-align numeric columns.
record Report(string Category, decimal Amount);
var reportData = new[]
{
new Report("Sales", 15000.00m),
new Report("Marketing", 8500.50m),
new Report("Operations", 12300.75m)
};
new DataGridControl(reportData)
.Resizable(false)
.WithColumn(new PropertyColumnControl<string> { Property = "category" }
.WithTitle("Category").WithResizable(false))
.WithColumn(new PropertyColumnControl<decimal> { Property = "amount" }
.WithTitle("Amount").WithAlign("end").WithFormat("C2"))
Table with action column
A complete employee directory showing name, email, department, and a "View Details" action — a common production pattern.
record Employee(string Name, string Email, string Dept);
var employees = new[]
{
new Employee("Alice", "alice@co.com", "Engineering"),
new Employee("Bob", "bob@co.com", "Marketing"),
new Employee("Carol", "carol@co.com", "Sales")
};
new DataGridControl(employees)
.WithColumn(new PropertyColumnControl<string> { Property = "name" }.WithTitle("Name"))
.WithColumn(new PropertyColumnControl<string> { Property = "email" }.WithTitle("Email"))
.WithColumn(new PropertyColumnControl<string> { Property = "dept" }.WithTitle("Department"))
.WithColumn(new TemplateColumnControl(Controls.Button("View Details"))
.WithTitle("").WithWidth("120px").WithSortable(false))
.Resizable(true)
.WithShowHover(true)
Row selection
A grid that feeds a bulk action ("Approve selected") declares its selection — it never builds one
from checkbox buttons and a hand-rolled state machine. WithRowSelection gives the grid a
selection column: a checkbox per row and a header checkbox, both bound to the layout area's
data section.
const string selectionId = "inboxSelection"; // one per grid; no '/' in the id
host.SeedRowSelection(selectionId); // where the grid is RENDERED — seeds once per session, a re-render keeps the ticks
Controls.Stack
.WithView(new DataGridControl(rows)
.WithColumn(new PropertyColumnControl<string> { Property = "title" }.WithTitle(host.Localize("…")))
.WithRowSelection(selectionId, keyProperty: "path", disabledReasonProperty: "blockedReason"))
.WithView(Controls.Button(host.Localize("…approveSelected"))
.WithReactiveClickAction(ctx => ctx.SelectedRowKeys(selectionId)
.Select(keys => DataGridRowSelection.Prune(rows, keys, r => r.Path, r => r.BlockedReason))
.SelectMany(toApprove => ApproveAll(ctx, toApprove)) // reports progress, see below
.Select(_ => Unit.Default)));
| Piece | What it does |
|---|---|
| row checkbox | toggles that row's key in the selection; a row whose disabledReasonProperty is non-empty renders disabled, with the reason as its tooltip, and can never be ticked |
| header checkbox | unchecked → selects every selectable row; indeterminate or checked → clears the selection. "All" means all selectable rows, so a grid with blocked rows still reaches the checked state |
RowKey |
set by WithRowSelection (or alone with WithRowKey(property)): the client carries each row's key value as RowContext.Key on a row-scoped click, so every row's busy state and Cancel are its own |
DataGridSelectionState |
what the client writes under the data id: { keys: [...] }, in selection order |
ctx.SelectedRowKeys(id) |
the selection as it stands at the click — a one-off read to return from the click handler |
host.RowSelection(id) |
the live selection — for a "3 selected" label or a bulk button's Disabled binding |
DataGridRowSelection.Prune |
the owner's re-check before acting: drops keys whose row is gone or no longer selectable. A selection is the viewer's claim about the rows they saw, never a grant |
The rules (select-all, header state, toggle) live in ONE place, DataGridRowSelection, which the
client views call and the owner re-applies, so what the header selects and what the action may act
on cannot drift apart. The bulk button is an ordinary button: it gets the framework's busy state,
progress line, summary and Cancel (Buttons: Pending State) — call
ctx.ReportProgress("Approving 3 of 7", 3 / 7.0) as it goes and ctx.ReportSummary(…) at the end.
🚨 Keep
/out of the selection data id: a client write into a data id containing/does not reach the owner today.
See Also
- Editor Control — Form generation
- Stack Control — Layout container