Form XML Reference

July 3, 2026 · View on GitHub

Looking for the runtime? The declarative loader on this page sits on top of the FormControl — read that first for the field semantics, validation model, and value access. This page documents the XML that describes a form.

SharpConsoleUI can build a form from a small declarative XML document instead of imperative builder calls. You describe the fields in a file (or a string constant), and FormXml parses it and drives the existing FormControl API to construct a live, fully-interactive form:

using SharpConsoleUI.Controls.Forms;

var form = FormXml.FromXml(@"
<form title='Contact'>
  <text name='name'  label='Name:'  required='true'/>
  <text name='email' label='Email:' pattern='^[^@]+@[^@]+$' message='Enter a valid email'/>
  <buttons/>
</form>");

window.AddControl(form);

This is the same XAML-family idea used across UI toolkits — markup describes the tree, code supplies the behavior. FormXml is a thin call-through: it parses the XML with the BCL XDocument and calls the FormControl field-add methods. It invents no layout, validation, or submit logic of its own — everything a loaded form does is exactly what the FormControl runtime does. It adds no dependency and is NativeAOT-safe (no reflection, no dynamic code; a pattern= compiles a runtime interpreted Regex).

Contents

The <form> root

The root element must be <form> — any other root throws a FormXmlException. Child elements are added to the form in document order.

<form title='Settings' columnGap='1' rowGap='0'>
  <!-- fields, sections, rows, buttons -->
</form>
AttributeTypeDefaultDescription
titlestringAccepted for readability/labeling. Note: FormControl renders no built-in title band — host the form in a titled window if you want a heading.
columnGapint0Sets FormControl.ColumnGap (space between the label and editor columns).
rowGapint0Sets FormControl.RowGap (space between rows).

Field elements

Each field element requires a name (the value key returned by GetValues()). A field with no name throws. A label is optional (defaults to empty). Every field also accepts an optional hint that renders a dim line beneath the editor.

<text>

Single-line text field (backed by PromptControl). Supports all validation attributes.

<text name='host' label='Host:' initial='localhost' required='true' hint='e.g. localhost'/>
AttributeTypeDefaultDescription
namestring— (required)Value key
labelstring""Label text
initialstring""Initial text
hintstringDim hint below the editor
widthintFixed input width in columns. Omit to stretch-fill the editor cell (default).
alignleft|center|right|stretchOverrides the field's default alignment (text stretch-fills). Omit for the smart default. Combines with width.
(validation attrs)See Validation attributes

<multiline>

Multi-line editor (backed by MultilineEditControl).

<multiline name='notes' label='Notes:' height='4' hint='Markdown accepted'/>
AttributeTypeDefaultDescription
namestring— (required)Value key
labelstring""Label text
initialstring""Initial content
heightint3Editor viewport height in rows
hintstringDim hint
widthintFixed editor width in columns. Omit to stretch-fill the editor cell (default).
alignleft|center|right|stretchOverrides the field's default alignment (multiline stretch-fills). Omit for the smart default. Combines with width.

Validation attributes on <multiline> are ignored in this version — see non-goals.

<checkbox>

Boolean checkbox (backed by CheckboxControl). Its label rides in the editor column. Value is "true" / "false".

<checkbox name='ssl' label='Use SSL/TLS' initial='true' hint='encrypt the connection'/>
AttributeTypeDefaultDescription
namestring— (required)Value key
labelstring""Checkbox label (editor column)
initialboolfalseInitial checked state
hintstringDim hint

Single-select dropdown (backed by DropdownControl). Options come from a comma-separated options list.

<dropdown name='driver' label='Driver:' options='PostgreSQL,MySQL,SQLite' initial='PostgreSQL'/>
AttributeTypeDefaultDescription
namestring— (required)Value key
labelstring""Label text
optionscomma-separated""Selectable options (empty entries dropped)
initialstringInitially selected value
hintstringDim hint
widthintFixed editor width in columns. Omit to stretch-fill the editor cell (default).
alignleft|center|right|stretchOverrides the field's default alignment (dropdown auto-fits to content, i.e. left). Use stretch to fill the cell. Omit for the smart default.

Validation attributes on <dropdown> are ignored — a dropdown always holds one of its options.

<radio>

Single-select radio group. Options come from a comma-separated options list; initial selects one.

<radio name='mode' label='Mode:' options='Read-write,Read-only,Replica' initial='Read-write'/>
AttributeTypeDefaultDescription
namestring— (required)Value key
labelstring""Label text
optionscomma-separated""Option strings (value = label)
initialstringInitially selected option

Validation attributes on <radio> are ignored — a radio group always resolves to an option.

width and align attributes are not honoured for <radio> — radios keep their natural, left-packed width (a filled radio group reads oddly). The imperative typed AddRadio<T>(…, width:, align:) overload accepts both, but the XML path uses the params-string overload, which takes neither.

<slider>

Numeric slider (backed by SliderControl). The slider clamps to its min/max range.

<slider name='timeout' label='Timeout (s):' min='0' max='60' initial='30' hint='connection timeout'/>
AttributeTypeDefaultDescription
namestring— (required)Value key
labelstring""Label text
minnumber0Minimum value
maxnumber100Maximum value
initialnumber0Initial value
hintstringDim hint
widthintFixed editor width in columns. Omit to stretch-fill the editor cell (default).
alignleft|center|right|stretchOverrides the field's default alignment (slider stretch-fills). Omit for the smart default. Combines with width.

Validation attributes on <slider> are ignored — the slider already clamps to its range.

Structure elements

<section>

Inserts a full-width section header row. Every field nested inside the <section> element belongs to that section; the section is closed automatically at the element's end.

<section title='Advanced' collapsible='true' collapsed='true'>
  <checkbox name='ssl' label='Use SSL/TLS'/>
  <slider name='timeout' label='Timeout (s):' min='0' max='60' initial='30'/>
</section>
AttributeTypeDefaultDescription
titlestringSection header text
collapsibleboolfalseAdds a ▸/▾ toggle to collapse the section
collapsedboolfalseStart collapsed (fields hidden). Only meaningful with collapsible='true'

<row>

Packs its child field elements side by side onto a single grid row (each field keeps its own label/editor pair). Children are the same field elements.

<row>
  <text name='first' label='First:'/>
  <text name='last'  label='Last:'/>
</row>

<row> takes no attributes of its own.

<buttons>

Adds the right-aligned action button row. The OK button calls Submit(); the Cancel button raises Cancelled.

<buttons ok='Connect' cancel='Cancel' showCancel='true'/>
AttributeTypeDefaultDescription
okstring"OK"OK button caption
cancelstring"Cancel"Cancel button caption
showCancelbooltrueWhether to include the Cancel button

Validation attributes

These attributes apply to <text> fields (and <multiline> in principle, but multiline is unvalidated in this version — see non-goals). Each present attribute contributes one rule; the field's value passes only when every rule passes. Empty values skip all built-in rules; only required fails on empty (so an optional field with a type/pattern/min/max/maxLength constraint is still allowed to be blank).

AttributeTypeRule
requiredboolValue must be non-empty ("Required").
typeint | numberValue must parse as a whole number (int) or a number (number).
patternregexValue must match the regular expression (compiled at load time via new Regex(pattern)).
minnumberNumeric value must be min.
maxnumberNumeric value must be max.
minLengthintString length must be minLength.
maxLengthintString length must be maxLength.
messagestringOverrides the default error text for every rule on the field.
<text name='port' label='Port:' type='int' min='1' max='65535' required='true'
      message='Enter a port between 1 and 65535'/>

Named validators (rule=)

For validation that XML can't express (cross-field checks, DNS lookups, custom parsing), attach a rule='name' to a <text> field and supply the C# implementation through the namedValidators registry passed to FromXml / FromXmlFile. The registry maps a name to a Func<string?, string?> that returns null when the value is acceptable, or an error message otherwise. A rule= whose name is not in the registry throws a FormXmlException.

var registry = new Dictionary<string, Func<string?, string?>>
{
    ["validDsn"] = v =>
        string.IsNullOrEmpty(v) || v.Contains('=')
            ? null
            : "Expected key=value pairs, e.g. Host=localhost;Port=5432",
};

var form = FormXml.FromXml(@"
<form>
  <text name='dsn' label='DSN:' rule='validDsn'/>
  <buttons/>
</form>", registry);

A rule= composes with the built-in validation attributes — the named validator runs alongside required, type, pattern, and the others on the same field.

Loading: FromXml / FromXmlFile

// From a string:
FormControl form = FormXml.FromXml(xml);
FormControl form = FormXml.FromXml(xml, namedValidators);

// From a file (root must be <form>):
FormControl form = FormXml.FromXmlFile("forms/connect.xml");
FormControl form = FormXml.FromXmlFile("forms/connect.xml", namedValidators);

Both return a ready FormControl. Wire submission/cancellation and read values with the runtime API — OnSubmit isn't an XML concept, so subscribe in code:

form.Submitted += (_, values) => Save(values["host"], values["port"]);
form.Cancelled += (_, _) => window.Close();

Errors: FormXmlException

FormXml never fails silently. It throws a FormXmlException (with line/position context where the XML provides it) when:

  • the XML is malformed (wraps the underlying XmlException),
  • the root element is not <form>,
  • an element is unknown (e.g. <slidr>),
  • a field is missing its required name attribute,
  • an integer/number attribute (columnGap, height, min, …) is present but unparseable,
  • a rule='name' references a validator not found in the registry.

Unknown elements throw a FormXmlException, but unknown attributes are silently ignored (for forward compatibility).

try
{
    var form = FormXml.FromXml(xml, registry);
}
catch (FormXmlException ex)
{
    // ex.Message includes "(line N, position M)" where available
    logger.LogError(ex, "Bad form descriptor");
}

v1 non-goals

The current loader deliberately stops here:

  • No <field> custom-editor element. The FormControl.AddField escape hatch (arbitrary editor + value getter) has no XML mapping — custom editors are wired in code.
  • No ToXml / export. The mapping is XML → form only; there's no form → XML serializer.
  • Validation on <dropdown>, <radio>, <slider> is ignored. Those FormControl overloads take no validator, and each type already constrains its own value.
  • <multiline> is unvalidated. The multiline overload takes no validator, so validation attributes on <multiline> have no effect.

Worked examples

1. Simple contact form

var form = FormXml.FromXml(@"
<form title='Contact'>
  <text name='name'  label='Name:'  required='true'/>
  <text name='email' label='Email:' pattern='^[^@\s]+@[^@\s]+\.[^@\s]+$'
        message='Enter a valid email address'/>
  <dropdown name='category' label='Category:' options='Bug,Feature,Question'/>
  <multiline name='message' label='Message:' height='5' hint='Describe the issue'/>
  <buttons/>
</form>");

form.Submitted += (_, values) =>
    windowSystem.NotificationStateService.ShowNotification(
        "Submitted", $"{values["name"]} — {values["category"]}", NotificationSeverity.Success);

window.AddControl(form);

2. Connection form with a collapsed Advanced section, validation, and a named rule

var registry = new Dictionary<string, Func<string?, string?>>
{
    ["validDsn"] = v =>
        string.IsNullOrEmpty(v) || v.Contains('=')
            ? null
            : "Expected key=value pairs (Host=…;Port=…)",
};

var form = FormXml.FromXml(@"
<form title='Connection' columnGap='1'>
  <text name='host' label='Host:' initial='localhost' required='true' hint='e.g. localhost'/>
  <text name='dsn'  label='Extra DSN:' rule='validDsn' hint='key=value;key=value'/>
  <dropdown name='driver' label='Driver:' options='PostgreSQL,MySQL,SQLite' initial='PostgreSQL'/>

  <section title='Advanced' collapsible='true' collapsed='true'>
    <text name='port' label='Port:' type='int' min='1' max='65535' initial='5432'
          message='Enter a port between 1 and 65535'/>
    <checkbox name='ssl' label='Use SSL/TLS'/>
    <radio name='mode' label='Mode:' options='Read-write,Read-only,Replica' initial='Read-write'/>
    <slider name='timeout' label='Timeout (s):' min='0' max='60' initial='30'/>
  </section>

  <buttons ok='Connect' cancel='Cancel'/>
</form>", registry);

form.Submitted += (_, values) => Connect(values["host"], values["port"], values["driver"]);
form.Cancelled += (_, _) => window.Close();

window.AddControl(form);

See also: FormControl · MARKUP_SYNTAX · CONTROLS

Back to Main Documentation