The Editor control turns a plain C# record into a fully interactive form — no markup, no field wiring. Annotate your properties with standard .NET attributes and the editor selects the right input control, applies validation, and streams live updates back to any reactive view you attach. C# Record string Name int Age bool IsActive DateTime BirthDate Attributes [Required] [DisplayName] [Range] [Browsable(false)] Reflection GetProperties() → MapToControl EditorControl TextFieldControl NumberFieldControl CheckBoxControl DateTimeControl Reactive View GetDataStream<T>() live re-render stream How the Editor works: record properties and attributes feed reflection, which maps each property to a typed field control; form changes stream to any attached reactive view.


Basic Usage

Simple form

Declare a record and call host.Edit(...). The editor reflects over every public property and renders an appropriate input:

public record Person
{
    public string Name { get; init; }     // → TextFieldControl
    public int Age { get; init; }         // → NumberFieldControl
    public bool IsActive { get; init; }   // → CheckBoxControl
}

host.Edit(new Person { Name = "Alice", Age = 30, IsActive = true })

The three properties above produce a text field, a number field, and a checkbox — in that order.

Form with reactive output

Pass a second argument to attach a live view that re-renders whenever the form data changes:

public record Calculator
{
    public double X { get; init; }   // first operand
    public double Y { get; init; }   // second operand
}

host.Edit(
    new Calculator { X = 10, Y = 5 },
    calc => Controls.Label($"Sum: {calc.X + calc.Y}")   // updates as you type
)

The label below the two number fields recalculates on every keystroke. The reactive variant wraps the EditorControl in a StackControl and subscribes to form changes via GetDataStream<T>().

Form with validation attributes

Standard .NET data-annotation attributes are picked up automatically:

public record UserProfile
{
    [Required]
    [Description("Your full name as it appears on documents")]
    public string FullName { get; init; }

    [DisplayName("Email Address")]
    public string Email { get; init; }

    [Range(18, 120)]
    public int Age { get; init; }

    [Browsable(false)]               // hidden — not rendered at all
    public string InternalId { get; init; }
}

host.Edit(new UserProfile { Age = 25 })

FullName shows a required indicator and a help-text line. Email carries the custom label. Age enforces the 18–120 range. InternalId is invisible to the user.


Property Type Mapping

The editor chooses a control for each property based on its declared type:

Property type Rendered control Typical example
string Text field public string Name { get; init; }
int, double, decimal Number field public decimal Price { get; init; }
bool Checkbox public bool Enabled { get; init; }
DateTime Date/time picker public DateTime BirthDate { get; init; }

These defaults apply only when no override attribute is present[UiControl<T>], [Dimension<T>], [MeshNode], [MeshNodeCollection], [Markdown], and [ContentItem] each substitute their own control. See Property Attributes for the full catalogue.


Supported Attributes

Apply any of these attributes directly to a property to alter how the field renders or validates:

Attribute Effect
[Required] Field cannot be empty; shows a validation error when blank
[Description("...")] Adds help text below the field
[DisplayName("...")] Replaces the auto-generated label with a custom one
[Browsable(false)] Hides the property entirely — it is never rendered
[Range(min, max)] Restricts numeric input to the given inclusive range
[Editable(false)] Renders the value as read-only; the user cannot change it
[MeshNode("query")] Searchable mesh-node picker; stores the selected node's path
[MeshNodeCollection("query")] Full-width inline chip collection of node references
[Markdown] Markdown display + editor with preview / track-changes options
[ContentItem("collection")] Text field + Browse button over a content collection

Control Override

When the default control is not the right fit, use [UiControl<T>] to substitute any compatible control:

public record Settings
{
    [UiControl<TextAreaControl>]
    public string Description { get; init; }   // multi-line instead of single-line

    [UiControl<RadioGroupControl>(Options = new[] { "Light", "Dark", "System" })]
    public string Theme { get; init; }          // radio buttons instead of a text field
}

host.Edit(new Settings { Theme = "System" })

Description expands to a full multi-line text area. Theme becomes a three-option radio group with "System" pre-selected.


Dimension Dropdowns

[Dimension<T>] populates a field with records from a data source, turning it into a searchable dropdown. The referenced type must declare a [Key] property:

public record Country
{
    [Key]
    public string Code { get; init; }
    public string Name { get; init; }
}

public record Address
{
    public string Street { get; init; }
    public string City { get; init; }

    [Dimension<Country>]
    public string CountryCode { get; init; }   // dropdown populated from all Country records
}

At render time, CountryCode fetches every Country from the data context and presents them in a dropdown keyed by Code.


Live Example

The form below is generated live from the record definition — each property becomes the field its type and attributes dictate (string → text field, int → number field, bool → checkbox, DateTime → date/time picker):

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

record Person
{
    [Required]
    [DisplayName("Full name")]
    public string Name { get; init; } = "Alice Example";

    [Range(18, 120)]
    public int Age { get; init; } = 34;

    [DisplayName("Active member")]
    public bool IsActive { get; init; } = true;

    [DisplayName("Birth date")]
    public DateTime BirthDate { get; init; } = new(1992, 3, 14);
}

Mesh.Edit(new Person(), "personDemo")

How It Works

Edit<T>() reflects over all public properties at startup:

typeof(T).GetProperties()           // enumerate every public property
    .Aggregate(new EditorControl(), // start with an empty editor container
        serviceProvider.MapToControl)  // add a named field for each property

Each property becomes a named area inside the EditorControl. The reactive overload wraps that control in a StackControl and appends a view that subscribes to form changes via GetDataStream<T>(), so the downstream view always sees the latest values.


Accessible names

A generated field paints its caption outside the input, and both generated forms do it differently:

Form Where the caption is painted What names the input
Edit<T>() / EditorControl the PropertySkin's <dt> a <label for> the renderer targets at the input's id, plus aria-label
The node editor's click-to-edit form (MapToToggleableControl) a sibling LabelControl above the field aria-label only — a FluentLabel carries no for, and there is no id to point it at

In both the control itself is created with no Label, so the caption is not drawn twice. That is deliberate, and it costs the input its accessible name unless something puts one back.

So the generators set AriaLabel on every generated input — every IFormControl: text, multi-line text, number, date, checkbox, switch, select, combobox, listbox, radio group, mesh-node picker.

Each form reads it from the same helper its own caption comes from, so within a form the accessible name and the visible term can never disagree — which is what WCAG's label in name asks for. The two helpers do not resolve the caption the same way, and that predates this:

Form caption helper resolution order
Edit<T>() GetEditorLabel [Display(Name = …)][DisplayName(…)] → the property name word-split
click-to-edit GetToggleableDisplayName [Display(Name = …)] → the viewer-localized [Description] / [Translation] → the property name word-split

🚨 So [DisplayName("…")] names an Edit<T>() field and does not name a click-to-edit one, which falls through to the wordified property name. That divergence is stated here rather than smoothed over: it is a question about the visible CAPTION, not about the accessible name, and changing it would re-word every click-to-edit caption in the portal.

public record Assessment
{
    [DisplayName("1. What topics do you want to cover?")]
    [UiControl<TextAreaControl>]
    public string Topics { get; init; } = null!;
}
// Edit<T>() renders: <dt><label for="property-…">1. What topics …</label></dt>
//                    <dd><fluent-text-area id="property-…" aria-label="1. What topics …"> …

🚨 aria-label is not decoration on top of <label for>. A Fluent input is a web component that keeps its real <input> inside a shadow root, and the generated form used to emit <label for="topics"> pointing at an element that could not exist: the caption was visible, the textbox was unnamed, and getByRole('textbox', { name: question }) matched nothing (MeshWeaver#3863). aria-label sits on the host element, needs no id plumbing, and is the only one of the two the click-to-edit form can use at all.

Setting Label yourself still works and still renders a second, visible caption — use it for a control you compose by hand, not for a field the editor generates. A control that carries its own Label is left alone: the Fluent host paints its own associated label, and an aria-label would only shadow it.

One generated surface is NOT covered, and it is named here rather than left to be rediscovered from a symptom: a [Markdown] / [UiControl<MarkdownEditorControl>] property. MarkdownEditorControl is not an IFormControl — it is a composite editor with its own toolbar and its own view, so its accessible name is an aria-labelledby question about that composite, not an aria-label on one input.


See Also

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.