Skip to content

Editor fields & datasets

<EditorFields> holds the create/edit form fields for a view. Each child is named after its field type. This page covers the field types and their attributes, plus the <Select> dataset system. The smart Required/Disabled/Hidden/Allow/Deny attributes and ValueExpression are introduced here but documented in full on Column & field expressions.

Name (required), Label, Required, Disabled, Hidden, Allow, Deny, Placeholder, DefaultValue, Width (1–12), ValueExpression, RegexValidation, and RegexMessage.

Required / Disabled / Hidden are “smart” attributes — each holds either a literal ("true"/"false") or a row-context expression. Allow / Deny are field-level security (whitelist / blacklist who may edit a field), server-enforced on submit. ValueExpression computes the field’s value. All of these are covered in detail under Column & field expressions. RegexValidation / RegexMessage and the <Validations> block are covered in Validation below.

A field’s DefaultValue is applied on submit whenever the caller supplied nothing for it, and every declared editor field is passed to the submit SQL — so @Field is always bindable in <SubmitSql>, <InsertSql> and <EditSql>, even for an API caller that omits the field entirely.

  • Create — absent or blank ⇒ DefaultValue (else NULL, or false for <Boolean>).
  • Edit — absent ⇒ the record’s stored value, else DefaultValue. A field the user explicitly cleared stays cleared and writes NULL: a default fills a gap, it doesn’t override an intent.

Field values are validated in two layers over one contract. The React form checks everything it can before the POST — blocking submit, showing the message under the field, and scrolling to + focusing the first offending field — and the server re-checks all of it on submit before any SQL runs. The server is the authority; the client pass is UX.

What is checked, in order:

  1. Required (literal or expression) — a blank value.
  2. RegexValidation — the pattern is unanchored on both layers (Regex.IsMatch ≡ new RegExp(p).test(v)), so add ^…$ yourself for a full match. RegexMessage overrides the generated message.
  3. Type bounds: <Number> MinValue/MaxValue/DecimalPlaces, <DateTime>/<Date>/<Time> MinDate/MaxDate, <Text>/<Password> MaxLength, <Password> MinLength, and a multiple <Select>’s MaxSelection. Each bound is checked on its own — a field declaring only MaxValue is still range-checked.
  4. The field’s <Validations> rules, in authored order.

Two rules apply to everything except Required:

  • A blank value is skipped. Demanding a value is Required’s job, so an optional field with a pattern or a rule is not flagged when left empty.
  • Fields the user can’t edit are skipped — hidden, disabled, Allow/Deny-locked, and <Sequence> (engine-generated). Their values aren’t the caller’s to fix, and pre-existing bad data must not block an unrelated edit.

Each <Validate> is an assertion: it must resolve truthy for the value to be valid, and a falsy result reports its Message. The rule body is the element’s text (use CDATA so </> need no escaping), and Type selects how it is evaluated — Expression (the default) or Sql.

<Text Name="Email" Label="Email"
RegexValidation="^[^@\s]+@[^@\s]+\.[a-z]{2,}$"
RegexMessage="Enter a valid email address." />
<Number Name="Discount" Label="Discount" DefaultValue="0">
<Validations>
<!-- Type="Expression" (default): evaluated in the browser AND re-checked on submit -->
<Validate Message="Discount cannot exceed the total."><![CDATA[ +@Discount <= +@Total ]]></Validate>
<Validate Message="Discount cannot be negative.">+@Discount >= 0</Validate>
</Validations>
</Number>
<Text Name="Code" Label="Code">
<Validations>
<!-- Type="Sql": server-only, evaluated against the submit parameters before the write -->
<Validate Type="Sql" Message="This code is already used by another product."><![CDATA[
SELECT CASE WHEN EXISTS (SELECT 1 FROM Inventory.Product
WHERE Code = @Code AND Id <> @Id)
THEN 0 ELSE 1 END
]]></Validate>
</Validations>
</Text>

Type="Expression" uses the same expression flavour as Required/Hidden (see Column & field expressions) and runs on both layers. An expression that fails to evaluate counts as a violation (fail-closed), so an authoring typo surfaces immediately rather than silently passing.

Type="Sql" is for checks a single row can’t see — uniqueness, referential state, aggregate limits. It runs server-side only, after every free check has passed and before the write:

  • The query receives the submit parameters (@Field for every declared field, the @Session* parameters, @FormLoadTime, and @Id — bound NULL on create, so the “unique except myself” shape @Id IS NULL OR p.Id <> @Id works in both modes) and must return a single scalar: truthy (1/true) is valid; falsy or no rows is a violation reporting Message.
  • The query is never sent to the browser (it’s stripped from /metadata like any other SQL body), so the client skips these rules; the submit response carries the message per field, and the form highlights + focuses that field exactly as it does for a client-side failure.
  • It is a read before the write, so a uniqueness rule narrows but does not close a race — keep the unique index as the real guarantee.
  • Imports skip SQL rules (one query per row per rule doesn’t pay); regex, bounds and expression rules still apply to every imported row.
<Events>
<On Type="Change"> <!-- Checked | Change | EnterPressed | Blur | RowEntered -->
<Script>field.value = (field.value || '').trim();</Script>
</On>
</Events>

Every type also takes the base attributes above, including RegexValidation and a <Validations> block.

Element Type-specific attributes
<Text> TextType (SingleLine|MultiLine|RichText|Email), MaxLength, Mask, AutoComplete, AutoCompleteSource
<Number> MinValue, MaxValue, DecimalPlaces, Step, ShowSpinButtons
<Boolean> DisplayType (Checkbox|Switch|RadioButtons), TrueLabel, FalseLabel
<Select> Searchable, Multiple, AllowClear, MaxSelection, ControlType (Select2|ModalSelector), EmptyOptionText, ImageColumn + a <DataSet>, and optional <Columns> / <ExtraMappings> (see below)
<DateTime> / <Date> / <Time> Format, MinDate, MaxDate, ShowCalendar, AllowManualInput (the element name picks date/time/both)
<Attachment> Accept, MaxFiles, MaxSize (bytes), Multiple, UploadPath
<Password> MinLength, MaxLength, HashedParameterName
<Sequence> Format (required), Scope (required)
<CartTable> inline editable grid — see CartTable

A <Select> field draws its choices from a <DataSet>. The Type picks the source.

<Select Name="CustomerType"><DataSet Type="static">
<Option Value="Individual" Text="Individual" />
<Option Value="Business" Text="Business" />
</DataSet></Select>
Type Source
static (or options) inline <Option Value="" Text="" />
csv comma-separated values in the element body
sql a SELECT … AS Value, … AS Label query in the body (+ LoadOnStart)
model Schema="" Model="" — reuses the static options of a <Select> field on that Genie entity: the target entity must declare a <Select> whose Name equals this editor field’s Name, and its <Option>s become the choices. For a lookup that lists another entity’s rows, use a sql dataset (SELECT Id AS Value, Name AS Label FROM …), not model.

A SQL dataset aliases the identifier as Value (submitted, hidden) and the display text as Label — or Text (the Select2 { id, text } convention is also accepted). If neither Label nor Text is present, the first column that isn’t Value is used as the label. Any other columns are “extras” — surfaced to the client for the modal table and <ExtraMappings>. The query self-filters using two server-injected parameters: @SearchText (%term% while typing, % when the box is opened with no filter) and @CurrentValue (the currently-selected value, for re-resolving its label). Session parameters (@SessionUserId, @SessionCompanyId, …) are injected too. View mode, and every field render before the dropdown is opened, resolves the stored value to its label through the same query — multi-selects resolve each comma-joined value.

Two paging parameters are also always injected — @PageSize and @PageOffset — so a dataset over a large table can page instead of returning every row. Use them with OFFSET @PageOffset ROWS FETCH NEXT @PageSize ROWS ONLY (SQL Server, needs an ORDER BY) or LIMIT @PageSize OFFSET @PageOffset (PostgreSQL). A query that ignores them is unchanged. A <CartTable> ModalSelector column drives these for scroll pagination; a plain dropdown leaves them unset, so the result stays uncapped. Label resolution never pages — when the control asks for the stored value(s), @PageSize is bound large at offset 0, so make the resolve branch return the exact rows (see the caution below): gate the browse/search branch on @CurrentValues = '' so paging can’t hide a stored value.

<DataSet Type="sql">
SELECT Id AS Value, Name AS Label, Email, Phone
FROM Sales.Customers
WHERE IsDeleted = 0
AND (@SearchText = '%' OR Name LIKE @SearchText OR Email LIKE @SearchText
OR CAST(Id AS NVARCHAR(20)) = @CurrentValue)
</DataSet>

Writing the resolve + @SearchText + @CurrentValue + OFFSET/FETCH plumbing on every dataset gets repetitive. <DataSet Type="search"> lets you write only the core query — columns + FROM + your base WHERE — and the engine generates the rest (label-resolution, type-to-search, ordering, and dialect-correct pagination):

<DataSet Type="search" Search="Label,SKU" OrderBy="Label">
SELECT p.Id AS Value, p.Name AS Label, p.Sku AS SKU, p.UnitCost AS UnitCost
FROM Sales.Products p
WHERE p.IsDeleted = 0 AND p.CompanyId = @SessionCompanyId AND p.IsActive = 1
</DataSet>
  • Search — comma-separated output aliases matched with LIKE while typing (defaults to Label).
  • OrderBy — output alias to order by (defaults to Label).
  • Still alias your key AS Value and display AS Label as usual; label-resolution stays correct automatically (it’s never paged).
  • Rules: the core query must not include its own ORDER BY or paging (the engine adds them — a stray ORDER BY raises a clear error); a leading WITH … CTE is fine (it’s hoisted).

It works everywhere a dataset does — editor-field selects, CartTable columns, and grid filters. Prefer Type="sql" (the full hand-written form above) only when you need something the shorthand can’t express (exotic resolve logic, per-row security, a computed value key).

Every <Select> renders as a styled, always-searchable dropdown — there is no raw native <select>. ControlType="Select2" (default) and Searchable="true" are accepted for back-compat but no longer change anything: a dropdown is searchable regardless, and a dataset-backed one searches server-side (LIKE via @SearchText) while a static-option one filters client-side. ControlType="ModalSelector" instead opens a picker dialog showing the dataset rows in a table with a tick column on the left and a debounced search box at the top. Set Multiple="true" on any of them to select more than one value — stored comma-joined, capped by MaxSelection, and shown in the control as non-colored tag pills (remove a value by reopening the dropdown and unticking it). AllowClear (default true) adds a clear (×) button to a single-select; EmptyOptionText/Placeholder set the empty-state text.

Optional. Chooses, labels, and orders the columns shown in the ModalSelector table. Omit it and the table shows every dataset column except Value.

<Columns>
<Column Name="Label" Label="Customer" Width="40" /> <!-- Name = dataset column; Label/Width optional -->
<Column Name="Email" Width="35" />
<Column Name="Phone" />
<Column Name="Spec" Label="Spec Sheet" IsAttachment="true" /> <!-- attachment tags, not a path -->
</Columns>

IsAttachment="true" marks a column whose value is an attachment path — the same pipe/comma-separated form an <Attachment> field stores. The cell renders attachment tags (image thumbnails with a lightbox, file-type tiles otherwise) instead of printing the stored path as text, exactly like a table <Column IsAttachment="true">. Bytes are fetched through the authorized file endpoint, so no <img src> needs a token.

Optional. Names a dataset column holding an attachment path, and the picker shows that file as a compact thumbnail beside each option’s label — in the dropdown list, on the collapsed control, and in the ModalSelector table. It works on both control types and on a <Select> used as a <CartTable> column, which is where it pays off most: identifying a design by its sketch rather than by its code.

<Select Name="DesignId" Label="Design" ControlType="ModalSelector" ImageColumn="Sketch">
<DataSet Type="sql">
SELECT d.Id AS Value, d.Name AS Label, d.Category, v.Sketch
FROM Designs d LEFT JOIN DesignVersions v ON v.Id = d.CurrentVersionId
</DataSet>
<Columns>
<Column Name="Sketch" Label="Khaka" /> <!-- attachment implied: it IS the ImageColumn -->
<Column Name="Label" Label="Design" Width="45" />
<Column Name="Category" Width="30" />
</Columns>
</Select>

The dataset SQL must SELECT the column under exactly that name — it is an extra column, so it also has to survive into the option rows (see SQL datasets). A column named as the ImageColumn counts as an attachment in the picker table without repeating IsAttachment on it. Only the first file of a multi-file value becomes the thumbnail; the picker cell still shows all of them. Rows whose image column is empty simply render no thumbnail, so a partly-populated catalog stays usable.

Optional. When a row is picked, copies columns from that row into other form fields — e.g. choose a customer and auto-populate read-only email/phone fields. (For a multi-select, the most-recently ticked row is applied.)

<ExtraMappings>
<Map Column="Email" Field="CustomerEmail" /> <!-- row column -> target field name -->
<Map Column="Phone" Field="CustomerPhone" />
</ExtraMappings>

Worked example: sample/Inventory/models/views/Products.view.xml demonstrates every field type, including a model dataset (reusing the Product.Status enum), a sql lookup (Category), a ModalSelector with <Columns> + <ExtraMappings> (Supplier → autofill email), a multi-select, and RichText.