Summary#
A component that works over data it doesn't know the shape of needs to be told how to get a value, not just which value:
[Composable] component Labelled(string name, Func<string, string> pick) {
render { Text(pick(name)); }
}
[Page("/")] [AllowAnonymous]
component Home() {
render {
Stack(p: 4) {
Labelled(name: "ada", pick: x => x + " (picked)");
Labelled(name: "grace", pick: x => "<" + x + ">");
}
}
}pick is a function value. It is passed as a lambda and invoked with pick(name) wherever a value is wanted.
Signature#
Func<T, R> // a parameter that takes a lambda of one argument returning R
Func<T1, T2, R> // …of two
pick(row) // invoke it for its valueContrast Action / ()-delegate parameters, which are callbacks: they run and produce nothing. A Func<>
produces a value and can stand anywhere a value can.
Description#
It is an expression, not a closure#
A function value is its parameter names, its body, and the values it captured. It is evaluated by binding the
parameters and evaluating the body — the same thing a query lambda (Where(r => r.Total > 0)) has always been.
It captures the surrounding scope, by value#
The body may read its parameters and whatever is in scope where you wrote it. Each outer name is read once, where the lambda literal appears — not later, where it is invoked:
component Roster() {
var admins = RoleGrant.Where(g => g.IsAdmin); // the page's own query
// `admins` is captured: read here, when this column list is built.
var columns = [ new GridColumn<User> {
Name = "Role",
Value = u => admins.Any(g => g.User == u) ? "Admin" : "User"
} ];
}Why by value, and not read later. A function value travels: a column selector is handed to a grid and invoked
deep inside it, where the names your body reads do not exist at all — so a late read could only ever find nothing.
Reading at the literal is also the answer you want, because the expression that built the lambda re-evaluates when
its own inputs change. When admins reloads, the column list is rebuilt and a fresh selector replaces the old one.
The one thing to know: a captured value is a snapshot. If you mutate a captured list in place, a selector built before the mutation keeps the value it was given.
Everything about the call is checked#
A slot's declared type is its contract, and all three ways of getting it wrong are compile errors:
| you wrote | you get |
|---|---|
pick: (a, b) => a.Title for a one-argument slot | takes Func<Report, string> — 1 parameter(s), but the lambda declares 2 |
pick: x => x.Amount where the slot returns a string | the lambda must produce string — it produces int |
pick(a, b) on a one-argument function | this verb takes 1 argument(s) — got 2 |
So a renamed field breaks the build rather than quietly rendering a blank.
Where it can be used#
A component parameter, and a class field — so a descriptor object can carry its own selector, which is what lets a caller describe a set of columns, filters or sort keys as data:
class Column {
public Func<string, string> Value;
public string Label;
}
[Composable] component Grid(string[] rows, Column[] columns) {
render {
Stack(gap: 2) {
Row(gap: 3) { foreach (var h in columns) { Text(h.Label); } }
foreach (var r in rows) {
Row(gap: 3) { foreach (var c in columns) { Text(c.Value(r)); } }
}
}
}
}
[Page("/")] [AllowAnonymous]
component Home() {
string[] names = ["ada", "grace"];
render {
Grid(rows: names, columns: [
new Column { Value = r => "<" + r + ">", Label = "Wrapped" },
new Column { Value = r => r + "!", Label = "Banged" }
]);
}
}The descriptor class can be generic, so one shape serves every row type instead of being copied per entity — see Generic classes:
class Column<T> { public string Label; public Func<T, string> Value; }
new Column<Report> { Label = "Title", Value = r => r.Title }It is not only a UI thing#
Every example above is a component, but a function value is an ordinary value in an ordinary function too: a class field holds one, and invoking it produces a result wherever a value is wanted.
class Report { public string Title; public Report(string title) { Title = title; } }
class Column<T> { public string Label; public Func<T, string> Value; }
string TitleOf(string title) {
var col = new Column<Report> { Label = "Title", Value = r => r.Title };
return col.Value(new Report(title));
}The one limit: a function value cannot be held across an await that suspends. What would have to travel is a
body expression plus a captured environment, which is not a storable value — so invoke it before the await and hold
its result instead.
Examples#
[Composable] component Show(int n, Func<int, string> fmt) {
render { Text(fmt(n)); }
}
[Page("/")] [AllowAnonymous]
component Home() {
render {
Stack(p: 4) {
Show(n: 42, fmt: v => "n = " + v);
Show(n: 42, fmt: v => "[" + v + "]");
}
}
}See also#
- component — parameters, state and the render block
- Slot (child content) — the other way to let a caller decide content: a template rather than a value
- Cell template (your own content in a control's cell) — per-item templates, which a function value complements rather than replaces