Columns
A column is declared once for the whole grid, not templated for every row. That single declaration is also where sorting, filtering, editing and conditional style live — which is why there is no separate configuration step.
The shape of a declaration
Every column starts at a factory — Column for text and cell kinds, TypedColumn for numbers, dates and enums — and is then narrowed by fluent calls. The factory decides what the cell is; the calls after it decide what it does.
Column.Text<Account>("Name", a => a.Name) // what it is
.WithKey("name") // a stable identity
.Sortable(a => a.Name) // what it does
.WithGrouping()
.Fill()Each call returns the column, so order does not matter and nothing needs to be assigned to a variable first. The lambdas are the only place your record type appears — the grid itself never reflects over it. A key from WithKey has to be unique within the view; it is what a saved layout uses to find the column again, and capturing a layout throws for a column that has none.
Factories
Column.Text- A string you produce. The general case.
TypedColumn.Number- A
double?, right-aligned, with a display format. Sorts on the number and filters on it as a number, not as text. TypedColumn.Date- A
DateTime?with one format for display and commit, and a calendar picker for editing. Sorts chronologically. TypedColumn.Enum- An enum member, sorted in declaration order rather than alphabetically. The editor's option list is generated from the type, so adding a member is enough.
Column.Badge- A status mark: a label and a colour, both per row. The theme decides whether it draws as a filled pill, a dot with a label, or the label alone.
Column.Tag- The same idea, drawn as an outlined label for a secondary mark.
Column.Button- A drawn button with a fixed label. A click arrives as
grid.Api.Events.ButtonClickedwith the item index and the element id. Column.Checkbox- A drawn checkbox. With a setter, clicking toggles directly — no editor session.
Column.CheckboxDisplay- Draw-only and three-state:
bool?allows an indeterminate mark. Column.Composed- Several elements in one cell, laid out freely. See below.
Column.Sparkline·Bullet·Variance·MiniStack·ColumnSpark·HeatStrip·Donut- Micro-charts as cells. They are drawn per frame like any other cell, with no control behind them, and carry their values as text for copy, search and the screen reader.
Column.Expander- The chevron that opens a master-detail band.
Column.Tree- The indented label column of a tree grid.
TypedColumn.Number<Account>("Amount", a => (double?)a.Amount,
set: (a, v) => a.Amount = (decimal)(v ?? 0),
format: "N2")
TypedColumn.Date<Account>("Booked", a => a.Booked,
set: (a, v) => a.Booked = v ?? default,
format: "dd.MM.yyyy")
TypedColumn.Enum<Account, Status>("Status", a => a.Status,
set: (a, v) => a.Status = v,
display: s => s switch { Status.OnHold => "On hold", _ => s.ToString() })Leave set out and the column is read-only. The number and date getters return nullable types on purpose: clearing a cell has to be a legal state, or a figure entered by mistake could never be taken back out again.
The typed factories hand the sort and the filter the value itself, not its formatted text, and they read the search text in the column's own culture and format. That is the reason to prefer them over Column.Text with a ToString: a thousands separator sorts as a character, and a localised date parses as a different day on a different machine. An edit the column cannot parse is refused and the old value stays, rather than being stored as zero.
Behaviour
Filtering is on by default and everything else is opt-in. A column that declares no sort key is a column you can only filter and look at — the right default for an Id.
.Sortable(a => key)- Header click cycles ascending, descending, off. Shift-click adds a level for multi-sort. The key is what is compared, so it can differ from what is shown. The typed factories set it for you.
.NotFilterable()- Every column with display text gets a box in the filter row under the header, and a funnel that opens a value list. This takes both away. Typing in the filter row is debounced.
.WithGrouping(key, label)- Offers the column for grouping, multi-level. Both arguments are optional: without them the sort key groups. Pass them to group by something coarser —
a => a.Booked.Datecollapses a timestamp to a day. Grouping itself is a Pro package; see the note below. .Editable((a, text) => …)- Double-click or F2 opens a self-drawn editor; the callback receives the committed text. The typed factories wire this for you when you pass a setter.
.Searchable(a => text)- What the filter, search and copy should use when the cell draws something that is not text — a composed cell, a button.
.SpeakAs(a => text)- What a screen reader says for the cell when the visual text does not read aloud — a checkmark glyph should say "active".
Group rows, the group band above the header and the group summaries come from ArionUI.Grouping, free during the preview. Install it with dotnet add package ArionUI.Grouping --prerelease and enable it per view with view.EnableGrouping(). Without that call a column marked WithGrouping simply does not group.
Width and alignment
.WithWidth(px)·.Fill(weight)- A fixed width, or a share of what is left over. A column with both treats its width as a minimum. Users can still drag the separator, double-click it to auto-size, or drag the caption to reorder.
.Right()·.Center()- Alignment.
TypedColumn.Numberis right-aligned already — figures that do not line up on the decimal point cannot be read. .Ellipsis()·.Clip()·.WithWrap(lines)- What happens when the text is longer than the cell. Wrapping does not grow the row, so pair it with a row height that fits.
.Band("Contact", "Address")- Puts the column under a banded header. The path is the nesting.
Editing
// Drop-down over a fixed option list
Column.Text<Account>("Status", a => Label(a.Status))
.Editable((a, v) => a.Status = Parse(v))
.WithEditOptions(_ => AllStatusLabels, DropDownStyle.Classic)
// Calendar, always showing its button
Column.Text<Account>("Booked", a => a.Booked.ToString("dd.MM.yyyy"))
.Editable((a, v) => a.Booked = DateTime.Parse(v))
.WithDateEditor(a => a.Booked, commitFormat: "dd.MM.yyyy")
.WithDropDownButton(DropDownButtonMode.Always)
// Masked input, and a validator that returns an error message or null
Column.Text<Account>("Phone", a => a.Phone)
.Editable((a, v) => a.Phone = v)
.WithInputMask("+00 (000) 000-0000")
.Validate(v => v.Length < 6 ? "Too short" : null)Edits are undoable — Ctrl+Z and Ctrl+Y work in the grid — and repainting an edit touches only the affected rows, never the whole grid.
Aggregates
Five functions: Count, Sum, Avg, Min, Max. The same maths serves the summary footer and, with the grouping package, the group rollup, so the two cannot disagree. The footer is switched on at the view with WithSummaryFooter().
TypedColumn.Number<Account>("Amount", a => (double?)a.Amount, format: "N2")
.Summary(AggregateFn.Sum, a => a.Amount) // value function required
.Summary(AggregateFn.Avg) // reuses the one above
Column.Text<Account>("Name", a => a.Name)
.SummaryChoices(AggregateFn.Count) // Count needs no value
TypedColumn.Number<Account>("Rate", a => (double?)a.Rate)
.Summable(a => a.Rate) // offered in the Σ menu, none activeSum, Avg, Min and Max need a decimal projection; Count does not. Give it once — the first Summary or a Summable — and the rest reuse it. For anything the five do not cover, Summary(label, compute) takes a custom aggregate.
Conditional style
TypedColumn.Number<Account>("Amount", a => (double?)a.Amount, format: "N2")
.Style(a => a.Amount >= 90_000, foreground: Palette.Amber, bold: true)
.Style(a => a.Amount < 10_000, foreground: Palette.TextDim)Colours come from Palette, which reads the current theme — a literal here would survive the switch to dark and stop being legible. Rules that should colour a whole row rather than one cell are StyleRules passed to the view; both, and the order they resolve in, are on Theming and styling.
Rules the user edits at runtime — data bars, colour scales, icon sets, with a rule manager and a marker in the column list — are the ArionUI.Formatting package, free during the preview: view.Formatting().EnableFormattingMenus(grid.Api, view). Column.Style and StyleRule above are part of the open engine and need no package.
Composed cells
When one value per cell is not enough, Column.Composed takes a tree of elements instead. It is built through Cell, typed at construction and erased once, so nothing boxes while drawing — and hit testing runs the same layout maths, which is why a button inside a cell is clickable without being registered anywhere.
const int StatusButtonId = 1001;
Column.Composed<Account>("Tag and action",
Cell.Row(6,
Cell.Tag<Account>(a => a.Amount > 50_000 ? "LARGE" : "small",
a => a.Amount > 50_000 ? Palette.Amber : Palette.Blue),
Cell.Spacer(), // no width = takes what is left
Cell.Button("Status", StatusButtonId)))
.Searchable(a => a.Amount > 50_000 ? "LARGE" : "small")The containers are Cell.Row and Cell.Stack, both taking a spacing and their children. Inside them: Cell.Text, Badge, Tag, Checkbox, Button, Progress, Spacer, the micro-charts, and Cell.Draw when you want the surface yourself. A click on Cell.Button reports its element id through grid.Api.Events.ButtonClicked.
Column.Composed<Account>("Share",
Cell.Row(8,
Cell.Progress<Account>(a => (double)a.Amount / 100_000),
Cell.Text<Account>(a => (a.Amount / 1_000).ToString("0") + "k",
fontSize: 11, color: Palette.TextDim,
align: TextAlign.Right, width: 32)))
.WithWidth(140)Cell.Progress takes a fraction between 0 and 1, not a percentage. A composed cell still takes the calls above — .WithWidth, .Searchable, .Summable. It is a column like any other; only its drawing is richer.
