Summary#
An enum member has two faces. It has a stored value — the compact thing that lives in the column and that your
code compares against — and it has a label, the words a person reads on screen. [Label("…")] sets the label.
Without one, the label is simply the member's name.
You never have to convert between them. Show an enum-typed value anywhere in a screen and the label is what appears.
Signature#
enum <Name> {
[Label("<label>")] <Member>, // an explicit label
/// <description>
<Member>, // a doc comment becomes the member's description
}Description#
How do I change the words a member shows?#
A member name is an identifier, so it cannot contain spaces or punctuation. [Label] supplies the words:
enum OrganizationType {
/// A single person's own space.
Personal,
[Label("Team or company")] Team,
}
entity Organization {
[Required, MaxLength(100)] string Name;
OrganizationType Type = OrganizationType.Team;
}Team reads as Team or company. Personal declares no [Label], so it reads as Personal — the member
name is a perfectly good label when it already says what it means, and you should not add [Label("Personal")]
just to be explicit.
A doc comment on a member becomes its description — a longer sentence a control can show beside the label, such as the help text under an option in a dropdown. It is optional; a member with no doc comment simply has none.
How do I show an enum value on a screen?#
Anywhere a screen displays an enum-typed value, it displays the label:
[Page("/orgs")]
[Render(CSR)]
component OrganizationsPage() {
var orgs = Organization.ToList();
render {
Stack(gap: 2) {
foreach (var o in orgs) {
Row(gap: 3) {
Text(o.Name);
Text(o.Type); // "Team or company" — the label, not the stored value
}
}
}
}
}A grid column reads the same way: give it the member's key (Type) and the cell shows the label. You never write a
lookup, a mapping, or a switch to turn a value into words.
Because the label is resolved only for display, the value itself is untouched — o.Type == OrganizationType.Team
still compares against the stored value, exactly as it always did.
When you need the label as a STRING — .Label#
Displaying a value shows its label without being asked. But some props take a string, not a value to render — a
Button's label, a title, an aria name — and there the label has to be read. .Label is that read.
The difference is the SLOT, not the value — one table so it never has to be worked out:
| you write | you get | why |
|---|---|---|
Text(dish.Cuisine) | Street food — the label | a text slot RENDERS an enum value |
Badge(dish.Cuisine.Label, …) | Street food | a string PARAMETER takes a string, so read the label |
Text("in " + dish.Cuisine) | in StreetFood — the member name | + is a string op, and an enum's string form is its name (C#-exact) |
Text("in " + dish.Cuisine.Label) | in Street food | …so say .Label when you concatenate |
Row { Text("in "); Text(dish.Cuisine); } | in Street food | …or give the enum its own text slot |
⚑ The third row is the one that surprises, and it is not a compile error — it renders, just with the wrong words. Two generated apps in a row reasoned their way to the last row from first principles rather than reading it here.
enum Cuisine { [Label("Street food")] StreetFood, Thai, Nordic }
[Page("/cuisines")]
[AllowAnonymous]
component CuisineFilter() {
Cuisine picked = Cuisine.Thai;
action Pick(Cuisine c) { picked = c; }
render {
Row(gap: 2) {
foreach (var c in Cuisine.Members) {
Button(c.Label, onPress: () => Pick(c)); // "Street food", not "StreetFood"
}
}
}
}Cuisine.Members is every member of the enum, so a picker is a foreach and stays right when a member is added.
.Description, .Name, .Icon and .Tone read the rest of a member's presentation the same way — .Name is the
member's identifier ("StreetFood"), which is what .ToString() gives you and almost never what a person should
read.
⚠ They are properties, not methods: r.Label, never r.Label().
The stored string must differ from the name — [Value]#
Under [Type(string)] an enum member is stored by its name by default. When the stored string must differ from the
name — to match an external system, or to keep a stable code while the member is renamed — the [Value] attribute
sets it explicitly: [Value("team")] Team stores "team" while your code still writes OrganizationType.Team. Most
enums never need it; reach for it only when the storage string is a contract with something outside your app.
Which icon and tone stand for a member? — [Icon] and [Tone]#
A label is not the only thing a member has. "Cancelled" is usually also a cross, and usually also a danger — and those are facts about the member, not about the screen that happens to be showing it. Say them once:
enum Tone { Neutral, Success, Warning, Danger, Accent }
enum OrderState {
[Label("Active"), Icon(check), Tone(Tone.Success)] Active,
[Label("On hold"), Icon(pause), Tone(Tone.Warning)] OnHold,
[Label("Cancelled"), Icon(close), Tone(Tone.Danger)] Cancelled,
}Without this, every dropdown, cell, badge and header re-decides with its own if (state == Cancelled) chain — which
is how the same enum ends up red on one screen and grey on the next.
[Icon] names a declared icon; [Tone] names a declared tone. Icon(check) must name an icon your app can
draw — one it declares (an icons/check.svg) or one of the built-ins — written unquoted.
⚠ Here the name is BARE, and at a call site it is qualified. The attribute takes Icon(check); rendering the
same glyph directly takes Icon(Icons.Check). The two spellings are not interchangeable, and writing the qualified
form in the attribute is a compile error. Tone(Tone.Danger) is a qualified reference to a member of a tone enum
— the same shape as [Classification(DataClass.PII)] — so the referenced member is checked. A typo in either is a
compile error with a suggestion, not a blank space at runtime.
[Tone] names a tone and never a colour. [Tone(Tone.Success)] says the member is a success and lets the
design system decide what that looks like — so re-theming the app carries every enum with it, and a dark mode does not
need the enum edited. A hex here would put presentation in your domain model permanently.
Neither has a fallback. A member with no [Icon] has no icon, and a screen is free to show nothing — unlike the
label, where something must always be shown.
Changing a label is safe; changing a name is not#
The label is presentation, so you can reword it freely — no stored data refers to it. The member name, by
contrast, is what your code names (OrganizationType.Team), and under [Type(string)] it is also what the column
stores (unless a [Value] pins it). Reword the label when the words are wrong; rename the member only when the concept
is.
See also#
- icons — declaring the icons
[Icon(…)]may name - enum — declaring an enum and choosing how it is stored
- entity members — using an enum as a member type
- control — foreign UI controls (charts, grids, maps) — controls that show and edit an entity's values