Summary#
A column already knows how to read its value — that is what its selector is. So the chosen column names the sort, and no string key ever enters the picture:
entity Report { [MaxLength(80)] string Title; decimal Total; }
class Column<T> { public string Label; public Func<T, string> Value; }
// The rows are PASSED IN, so they are already fetched and sorting them is a local matter.
[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
Column<T> sortBy = columns[0];
action SortBy(Column<T> c) { sortBy = c; }
render {
Stack(gap: 2) {
Row(gap: 2) {
foreach (var h in columns) {
Pressable(onClick: () => SortBy(h)) { Text(h.Label, fontWeight: "600"); }
}
}
foreach (var r in rows.OrderBy(sortBy.Value)) { Text(sortBy.Value(r)); }
}
}
}
[Page("/")] [AllowAnonymous]
component Home() {
live var reports = Report.ToList();
render {
Grid(rows: reports, columns: [ new Column<Report> { Label = "Title", Value = r => r.Title },
new Column<Report> { Label = "Total", Value = r => r.Total.ToString() } ]);
}
}A string key would rename silently and fail at run time. A selector is checked against the row type, so a column that reads a field the row does not have is a compile error naming the row type.
Signature#
| form | where it runs |
|---|---|
rows.OrderBy(column.Value) | in memory, over rows the client already holds |
rows.OrderByDescending(column.Value) | in memory, descending |
Entity.OrderBy(column.Value).Take(n) | on the SERVER, before the page is taken |
Description#
Loaded rows sort in memory#
While the page is the table — every row is loaded — sorting the rows in hand sorts the table. OrderBy over a
list or a component's T[] parameter does exactly that, and nothing crosses the network.
Where the rows come from decides which sort you get, and it is not a detail. Rows passed in as a parameter have
already been fetched, so there is no read left to change and the sort is local. OrderBy written directly on a
read — an entity set, or a live var holding one — folds into that read instead, so the SERVER sorts. That is
what you want (sorting a page you were handed is the wrong question), and it is why the paged form below is the
same expression rather than a different one.
A PAGED read sorts on the server#
Once a read is paged, sorting the rows in hand is the wrong question: it re-orders the twenty rows you were given, when what you asked for is the first twenty of the ordered set. Write the same expression on the entity read and the server orders first:
entity Report { [MaxLength(80)] string Title; decimal Total; }
class Column<T> { public string Label; public Func<T, string> Value; }
[Page("/")] [AllowAnonymous]
component Home() {
var columns = [ new Column<Report> { Label = "Title", Value = r => r.Title },
new Column<Report> { Label = "Total", Value = r => r.Total.ToString() } ];
Column<Report> sortBy = columns[0];
action SortBy(Column<Report> c) { sortBy = c; }
live var page = Report.OrderBy(sortBy.Value).Take(20);
render {
Stack(gap: 2, p: 4) {
Row(gap: 2) {
foreach (var h in columns) {
Pressable(onClick: () => SortBy(h)) { Text(h.Label, fontWeight: "600"); }
}
}
foreach (var r in page) { Text(sortBy.Value(r)); }
}
}
}Changing the column re-runs the read, so the page can gain and lose rows — which is the point, and the difference you can see: the first twenty by name and the first twenty by total are not the same twenty.
Two rules the compiler enforces on a paged sort#
A paged sort is compiled ahead of time, so the columns it can sort by have to be knowable when it is compiled.
- The chosen column must be taken from the column list —
sortBy = columns[0]. That one line is what lets the compiler read every column the sort could use. - The column list must be written out where the component declares it. A list built by a function, or arriving as a parameter, has no contents to read, and a sort key guessed at would quietly return a different page.
Anything else is refused, naming what to change. Your own code never mentions how the choice reaches the server.
Sorting state belongs beside the read#
For a paged sort the chosen column and the read live in the same component, because the read is what the sort belongs to. A reusable grid takes the chosen column and a "sort by this" callback as parameters.
Examples#
entity Report { [MaxLength(80)] string Title; decimal Total; }
class Column<T> { public string Label; public Func<T, string> Value; }
// A selector is an ordinary expression, so a column can sort by something it COMPUTES rather than a stored field —
// which is the thing a string key could never have expressed.
[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
Column<T> sortBy = columns[0];
render {
Stack(gap: 2) {
foreach (var r in rows.OrderByDescending(sortBy.Value)) { Text(sortBy.Value(r)); }
}
}
}
[Page("/")] [AllowAnonymous]
component Home() {
live var reports = Report.ToList();
render {
Grid(rows: reports, columns: [ new Column<Report> { Label = "Band", Value = r => r.Total > 150 ? "high" : "low" },
new Column<Report> { Label = "Title", Value = r => r.Title } ]);
}
}See also#
- Func<T, R> — what a column's
Valueis, and why a selector rather than a string - generic component — one
Column<T>serving every row type - component — state, actions and the render block