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.
Base attributes (all field types)
Section titled “Base attributes (all field types)”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.
Default values
Section titled “Default values”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(elseNULL, orfalsefor<Boolean>). - Edit — absent ⇒ the record’s stored value, else
DefaultValue. A field the user explicitly cleared stays cleared and writesNULL: a default fills a gap, it doesn’t override an intent.
Validation
Section titled “Validation”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:
Required(literal or expression) — a blank value.RegexValidation— the pattern is unanchored on both layers (Regex.IsMatch≡new RegExp(p).test(v)), so add^…$yourself for a full match.RegexMessageoverrides the generated message.- Type bounds:
<Number>MinValue/MaxValue/DecimalPlaces,<DateTime>/<Date>/<Time>MinDate/MaxDate,<Text>/<Password>MaxLength,<Password>MinLength, and a multiple<Select>’sMaxSelection. Each bound is checked on its own — a field declaring onlyMaxValueis still range-checked. - 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.
<Validations>
Section titled “<Validations>”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 (
@Fieldfor every declared field, the@Session*parameters,@FormLoadTime, and@Id— boundNULLon create, so the “unique except myself” shape@Id IS NULL OR p.Id <> @Idworks in both modes) and must return a single scalar: truthy (1/true) is valid; falsy or no rows is a violation reportingMessage. - The query is never sent to the browser (it’s stripped from
/metadatalike 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.
Per-field events
Section titled “Per-field events”<Events> <On Type="Change"> <!-- Checked | Change | EnterPressed | Blur | RowEntered --> <Script>field.value = (field.value || '').trim();</Script> </On></Events>Field types
Section titled “Field types”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 |
Select <DataSet>
Section titled “Select <DataSet>”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. |
SQL datasets — Value / Label / extras
Section titled “SQL datasets — Value / Label / extras”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>Search datasets (shorthand)
Section titled “Search datasets (shorthand)”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 withLIKEwhile typing (defaults toLabel).OrderBy— output alias to order by (defaults toLabel).- Still alias your key
AS Valueand displayAS Labelas usual; label-resolution stays correct automatically (it’s never paged). - Rules: the core query must not include its own
ORDER BYor paging (the engine adds them — a strayORDER BYraises a clear error); a leadingWITH …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).
Control types
Section titled “Control types”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.
<Columns> (ModalSelector table)
Section titled “<Columns> (ModalSelector table)”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.
ImageColumn (thumbnails on the options)
Section titled “ImageColumn (thumbnails on the options)”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.
<ExtraMappings> (autofill other fields)
Section titled “<ExtraMappings> (autofill other fields)”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.