Model-Driven View Definition
Schema id: model-driven-view@2
Machine-readable schema: model-driven-view.schema.json
Superseded version: model-driven-view@1
(model-driven-view-v1.schema.json) stays
published and registered, so a Solution already deployed against it is still validated
against the schema it was authored to. Version 2 only ever adds optional properties — the
Shape table below is the current list — so every v1 definition is also a valid v2 one and nothing
has to be rewritten.
The canonical representation of a Model-Driven Apps view (Section 6.7) — a tabular list of records with its columns, filters, and sorts.
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
logicalName | string | yes | Publisher-prefixed, lower snake-case. |
displayName | string | yes | Human-readable view name. |
entity | string | yes | The owning entity's logical name. |
description | string | no | The line shown under the title on the view's title bar. |
icon | string | no | The icon shown on the view's title bar. |
viewType | string | no | What the view is for: Public (default), Lookup, QuickFind, Associated, QuickView, Card, SeedData. |
seedDataKind | string | on a SeedData view | Required on a SeedData view and refused on any other: Configuration (rows the solution needs to work; an import always brings them, Production included) or Demonstration (sample rows; imported only when Include demonstration data is ticked, never into Production). Carried into the package as the seed-data document’s kind. |
columns | array | yes | Field logical names shown as columns. |
filters | array | no | Flat filter clauses (ANDed): field, operator, and (for value operators) value. |
filterGroup | object | no | A hierarchical AND/OR filter tree (Advanced-Find style), ANDed with filters. |
sorts | array | no | Sort clauses: field, descending. |
actions | array | no | Designable command-bar actions. |
hierarchy | object | no | Present when the view renders as a tree rather than a grid. |
calendar | object | no | Present when the view renders as a calendar rather than a grid. Never alongside hierarchy. |
columnProperties | array | no | How individual columns are presented — header, width, format, alignment, control. Sparse. |
commandOverrides | array | no | What this view changes about the commands the platform owns. |
commandOrder | array | no | One bar, one order: built-in and maker commands interleaved, by id. |
rowSelection | boolean | no | Whether rows carry a selection tick box. true by default. |
Operators: Equals, NotEquals, Contains, GreaterThan, LessThan, IsNull,
IsNotNull. Every referenced field (columns, filters, filter-group conditions, sorts)
must exist on the entity.
viewType
A view's type says which surface reads it. One entity can carry a default view of each type at once, because they answer different questions:
| Value | What it is for |
|---|---|
Public | The entity's normal list page. This is the default, and what a definition with no viewType is. |
Lookup | The columns shown inside a lookup control's results panel — usually two to four. |
QuickFind | The find columns: the columns a typed search matches against. Its columns are the searched set; its filters and sorts are not read. |
SeedData | The rows and columns that travel with the solution (PF22). Never shown: a solution export runs it — its filters decide the rows, its columns the fields — and packs the result as a seed-data@1 document, which an import upserts by id. A table with several uses its default one, else the first by logical name. |
Associated | The related-records subgrid. Reserved — nothing renders it yet. |
A lookup control that names no view resolves one automatically: the target entity's
default Lookup view, else its default Public view truncated to its first three
columns, else the entity's primary field alone. A search with no QuickFind view
matches the primary field only. Find columns must be text fields, since the match is a
case-insensitive "contains".
calendar (a view that renders as a calendar)
A view with a calendar block draws its records placed on a day, week or month sheet — by two of
its own date fields — instead of as a grid. Absent is the grid, exactly as with hierarchy, so every
stored view stays valid. A view carrying both blocks is refused: a view is drawn one way.
It is a mapping, not a data model. There is no booking table and no appointment type: any table with a date on it can be laid out on a calendar, and which of its dates means "starts" is the maker's to say.
| Field | Type | Required | Notes |
|---|---|---|---|
start | string | yes | The Date and Time field a block is placed at. The only required role: a record with no start cannot be placed on a sheet, so there is nothing to fall back to. A system field is allowed — created_at_utc lays records out by when they arrived. |
end | string | no | The Date and Time field a block ends at. Absent is for a table recording a moment rather than a span, and makes every block defaultDurationMinutes long — not zero, because a block with no height cannot be clicked. |
title | string | no | The field the block's first line reads from. Absent means the table's primary field, which is what a grid, a lookup panel and a breadcrumb already show. |
category | string | no | A Choice, State or Reason field whose options tint the blocks and fill the legend. |
bookedFor | string | no | The field naming who the slot is for, shown quietly under the title. Any type: a lookup resolves to the target record's name. |
note | string | no | The field shown on a block tall enough for a third line, and in its tooltip otherwise. |
defaultScale | string | no | Which sheet the view opens on: Day, Week (default) or Month. |
startHour | integer | no | The first hour the day and week sheets show, 0–23. 0 by default. |
endHour | integer | no | The hour those sheets end at, 1–24, and greater than startHour. 24 by default. |
defaultDurationMinutes | integer | no | How long a slot is when the reader clicks an empty one — what the new record arrives with — and the height of every block on a view mapping no end. At least 1; 60 by default. |
🔴 start and end must be Date and Time fields, and category must carry colours. Both are
refused on save, because both fail silently on screen: a start that is a text column draws an
empty sheet, which reads as "nothing is booked this week" — the one conclusion a reader will act
on and cannot check. The colour lives on the Choice's own options, which is what lets "Exam is amber"
be said once and be read by the calendar, a grid cell and a chart alike.
startHour and endHour are the difference between a usable booking sheet and one that opens
two-thirds empty: a clinic working 8 to 7 does not want sixteen rows of midnight. They say where the
sheet opens — the reader still switches scale and narrows the hours from the sheet's own header,
because the window is theirs rather than the maker's.
There is deliberately no resource role. The sheets are days-across, so a field naming the column
a block belongs in would be a mapping a maker could make and then watch do nothing. It arrives with
the axis that draws it.
hierarchy (a view that renders as a tree)
A view with a hierarchy block draws its records nested by a lookup — an org chart with the
people in each unit, a category tree, a bill of materials — instead of as a grid. Absent is the grid
every view has always been, which is why every stored view stays valid.
What a tree is for is navigation: rows are fetched a window at a time, so a chart of fifty thousand people opens without loading any of it.
| Field | Type | Required | Notes |
|---|---|---|---|
nodeTypes | array | yes | Every kind of node the tree can draw. At least one. |
order | array | no | The order segments under one node are drawn in, by node type id. Absent means declaration order. |
rootPicker | boolean | no | Whether a reader may focus the tree on one node — opening on a city rather than the whole organisation. |
expandRoots | boolean | no | Whether the first level opens on load. true by default. |
Each entry in nodeTypes:
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | What this type is called inside the block — unit, person. Unique here, and what order and every childOf.of refer to. The same table may appear twice in one chart, which is why this is the maker's own word rather than a table name. |
entity | string | yes | The table these nodes are records of. |
childOf | object | yes | How this type hangs under its parent — see below. |
label | string | no | The field whose value is the node's line. Absent means the table's primary field. |
secondary | string | no | The field shown quietly after the label. Absent means the table's own secondary field; an empty string means none — on a unit the parent's name is the row directly above it, so a chart is one place a table's default second line is wrong. |
icon | string | no | The glyph on the row. Absent means the table's own icon. |
image | boolean | no | Whether the row carries the record's picture. |
detail | object | no | What fills the pane beside the chart when one is selected. Absent means the table's Main form. |
childOf takes a field — the Lookup on this type's table holding the parent record — and an
optional of, the node type it hangs under by id.
🔴 Exactly one node type leaves of out. That one is the tree's own table nesting under itself,
and its records with an empty parent are the chart's top level; every other type names what it hangs
under. Heterogeneous nodes are the general case rather than a bolt-on: units, then the people in
them is the same mechanism as province → city → branch modelled as three tables.
The lookup named by childOf.field must target the parent type's table — its own, for the root
type. One pointing anywhere else does not describe a nesting, and is refused on save rather than
rendering a level with nothing under it.
detail takes a kind of Form (the table's own form, saved through the record form's write
path), Card (a Card view laid out to be read rather than typed into, which is what a person under
a unit usually wants) or None (for a type that is only ever structure), plus an optional name
for which form or Card view.
columnProperties (how a column is presented)
A sparse list: only columns a maker configured appear, and a column with no entry renders from
its field metadata alone. It sits beside columns rather than turning that into a list of objects,
so every v1 definition stays valid and every reader that only needs to know which columns a view
has is untouched.
| Field | Type | Required | Notes |
|---|---|---|---|
field | string | yes | The column this configures. Must be one of the view's columns. |
label | string | null | no | Header override. Absent uses the field's display name. |
width | integer | null | no | Width in pixels, 40–1200. Absent lets the column size itself. |
format | string | null | no | Value format. For a DateTime column, the same token set the form's date controls accept (yyyy, yy, MM, dd, HH, mm, ss) — one formatting vocabulary across the product. |
align | string | null | no | start, center or end. Logical, not left/right, so it follows the reading direction (§6.13.6). |
controlType | string | null | no | The control the column renders with — a switch glyph for a boolean, a Persian date for a date. |
controlType takes Text, TextArea, Check, Toggle, Date, DateTime, Time,
PersianDate, PersianDateTime, PersianTime, Lookup, PolymorphicLookup, Dropdown or
Label. Absent inherits the field's own default and then the data type's.
🔴 It is always rendered read-only. A grid has no inline editing, so a switch here shows state
rather than taking a click. It is also the only way a column can render Jalali in a Gregorian
tenant, since format is a pattern with no calendar attached.
commandOverrides (changing a command the platform owns)
What this view changes about the built-in commands — New, Edit, Delete, Refresh, Save, Save & Close, Pin. Absent changes nothing, which is what every view authored before this existed means.
| Field | Type | Required | Notes |
|---|---|---|---|
command | string | yes | The stable id of the command being overridden. |
label | string | no | Replaces the platform's label. Absent keeps it — including its translations, which a typed override necessarily loses. |
icon | string | no | Replaces the platform's glyph. |
initiallyHidden | boolean | no | Whether the command starts hidden. |
initiallyDisabled | boolean | no | Whether the command starts disabled. |
🔴 Keyed by a stable command id, never by label or position. The id is the platform's own name and does not change when the label does, so an override cannot detach from what it overrides the moment somebody edits it — and a built-in can be renamed or retired later without every stored document needing reconciliation, since an override naming a command that no longer exists is simply ignored.
🔴 A maker may hide and disable, never show or enable. The Player hides New from somebody who cannot create and disables Delete with nothing selected; an override able to undo that would build a bar offering what the server will refuse.
"Initially" is the whole of the word. It is the state the bar opens in, not a permanent condition — a Business Rule or the command's own script may show it afterwards. A command that must never appear for somebody is not this; that is a permission, decided on the server where a browser cannot argue with it.
commandOrder (one bar, one order)
The built-in commands and the maker's own, interleaved as the maker arranged them, by id.
An explicit list rather than a rank on each command, because a rank must be rewritten on every command whenever one moves, and two commands can hold the same one — a tie the renderer would then break differently from however the designer broke it.
🔴 Anything this list does not mention keeps its natural place, at the end. That is what makes the rule survive time: a built-in added to the platform next year, or a command an upgrade adds, appears rather than vanishing because a stored order predates it. An id naming something that no longer exists is skipped.
rowSelection (whether rows carry a tick box)
true by default, and what every view authored before this existed means: the grid gets its
select-all header and per-row boxes, which is what the command bar acts on.
false is for a view that is only ever read — a list nobody deletes or reassigns from — where a
tick box offers a multi-select the surface has no use for and costs a column of width.
filterGroup (hierarchical AND/OR)
A filterGroup is a recursive node combining its members with a connector:
| Field | Type | Required | Notes |
|---|---|---|---|
connector | string | yes | And (all members match) or Or (any member matches). |
conditions | array | no | Leaf conditions (field, operator, value) in this group. |
groups | array | no | Nested sub-groups (same shape), for arbitrary AND/OR nesting. |
Example
{
"logicalName": "par_invoice_active",
"displayName": "Active Invoices",
"entity": "par_invoice",
"viewType": "Public",
"columns": ["par_name", "par_total"],
"filters": [ { "field": "par_total", "operator": "GreaterThan", "value": 0 } ],
"filterGroup": {
"connector": "And",
"conditions": [ { "field": "par_name", "operator": "Contains", "value": "INV" } ],
"groups": [
{
"connector": "Or",
"conditions": [
{ "field": "par_total", "operator": "GreaterThan", "value": 1000 },
{ "field": "par_total", "operator": "IsNull" }
]
}
]
},
"sorts": [ { "field": "par_name", "descending": false } ]
}
Example — a lookup view
{
"logicalName": "par_contact_pick",
"displayName": "Contact Lookup",
"entity": "par_contact",
"viewType": "Lookup",
"columns": ["par_name", "par_email", "par_city"],
"sorts": [ { "field": "par_name", "descending": false } ]
}
Example — a quick find view
Only columns is read: they are the columns a typed search matches against.
{
"logicalName": "par_contact_find",
"displayName": "Contact Quick Find",
"entity": "par_contact",
"viewType": "QuickFind",
"columns": ["par_name", "par_email", "par_accountnumber"]
}