--- name: overview-pages description: "The CRUD overview page pattern in MDL — a navigation snippet, a list page and a new/edit page wired together. Use when building the standard list-plus-edit screens for an entity." --- # Overview Pages - CRUD Page Pattern ## Overview Standard pattern for creating CRUD (Create, Read, Update, Delete) pages in Mendix using MDL syntax. This pattern consists of: 1. **Navigation Snippet** - Reusable menu for consistent navigation 2. **Overview Page** - Lists all objects with a DataGrid and navigation snippet 3. **NewEdit Page** - Form for creating/editing a single object ## Pattern Summary | Component | Type | Purpose | Key Widgets | |-----------|------|---------|-------------| | `Entity_Menu` | Snippet | Vertical sidebar navigation | NAVIGATIONLIST with ITEM actions | | `Entity_Overview` | Page | List all records | SNIPPETCALL (sidebar), DATAGRID, Heading | | `Entity_NewEdit` | Page | Create/Edit form | DataView, Input widgets, Save/Cancel | ## Navigation Menu Snippet Create a reusable navigation snippet using NAVIGATIONLIST for vertical sidebar menus: ```sql mdl 1; create snippet Module.Entity_Menu { navigationlist navMenu { item itemCustomers (caption: 'Customers', action: show page Module.Customer_Overview) item itemOrders (caption: 'Orders', action: show page Module.Order_Overview) item itemProducts (caption: 'Products', action: show page Module.Product_Overview) } }; ``` ### Snippet Syntax ```sql create [or replace] snippet Module.SnippetName [( params: ( $ParamName: Module.EntityType ) )] [folder 'path'] { -- Widget definitions (same as pages) } ``` ### NAVIGATIONLIST Syntax The NAVIGATIONLIST widget creates a vertical menu with navigation items: ```sql navigationlist widgetName { item itemName (caption: 'Caption', action: show page Module.PageName) item itemName (caption: 'Caption', action: call microflow Module.MicroflowName) item itemName (caption: 'Caption', action: close page) } ``` ## Overview Page Template Lists all objects of an entity type with a data grid and navigation menu in a sidebar layout. **Layout Structure:** ``` ┌─────────────────────────────────────────────┐ │ layoutgrid │ │ ┌────────┬──────────────────────────────────┤ │ │ COL 2 │ COL 10 │ │ │ menu │ Heading + datagrid │ │ │snippet │ │ │ └────────┴──────────────────────────────────┤ └─────────────────────────────────────────────┘ ``` ```sql mdl 1; create page Module.Entity_Overview folder 'OverviewPages' ( title: 'Entity Overview', layout: Atlas_Core.Atlas_Default ) { layoutgrid mainGrid { row { column (desktopwidth: 2) { snippetcall navMenu (snippet: Module.Entity_Menu) } column (desktopwidth: 10) { dynamictext heading (content: 'Entities', rendermode: H2) datagrid EntityGrid ( datasource: database Module.Entity, selection: Multi, PagingPosition: both, designproperties: ('Compact': on, 'Hover': on, 'Striped': on) ) { column (attribute: Name, caption: 'Name') { textfilter textFilter1 } column (attribute: Description, caption: 'Description') { textfilter textFilter2 } column (caption: 'Actions') { actionbutton btnEdit (caption: 'Edit', action: show page Module.Entity_NewEdit("entity" = $currentObject)) actionbutton btnDelete (caption: 'Delete', action: delete, buttonstyle: danger) } } } } } }; ``` ### SNIPPETCALL Syntax Include a snippet in a page using SNIPPETCALL: ```sql -- Simple snippet call snippetcall widgetName (snippet: Module.SnippetName) -- With parameters (for parameterized snippets): snippetcall widgetName (snippet: Module.SnippetName, params: (Customer = $Customer)) ``` ### Overview Page Components 1. **Navigation Snippet**: `snippetcall` referencing `Module.NavigationMenu` 2. **Layout**: `Atlas_Core.Atlas_Default` - Full page with header/footer 3. **Heading**: `dynamictext` with `rendermode: H2` 4. **Data Grid**: `datagrid` with `datasource: database` binding ### DATAGRID Syntax ```sql datagrid GridName ( datasource: database from Module.Entity where [IsActive = true] sort by Name asc, selection: Multi, PagingPosition: both, designproperties: ('Compact': on, 'Hover': on, 'Striped': on) ) { column (attribute: Name, caption: 'Name') { textfilter textFilter1 } column (caption: 'Actions') { actionbutton btnEdit (caption: 'Edit', action: show page Module.Entity_NewEdit("entity" = $currentObject)) } } ``` **Properties:** - `datasource: database from Module.Entity` - Entity data source (required) - `where [condition]` - Optional XPath filter (inline after entity in DataSource) - `sort by attr asc|desc` - Optional sorting (inline after WHERE: `sort by Name asc, Price desc`). A sort may navigate associations, one `/` per hop, with the last segment the attribute: `sort by Order_BillTo/City asc`. **Name the hop when more than one association reaches the same entity** — a bare `Module.Address.City` is resolved by inference, which cannot tell `Order_ShipTo` from `Order_BillTo`, and the wrong one builds cleanly and sorts by the wrong thing (mendixlabs/mxcli#1152) - `selection: Multi` - Multi-selection (`Multi`, `Single`, or omit for none) - `PagingPosition: both` - Pagination bar position (`top`, `bottom`, `both`) - `designproperties: ('Compact': on, 'Hover': on, 'Striped': on)` - Atlas design tokens **Column Types:** - `column (attribute: attribute, caption: 'label')` - Attribute column (own-entity attribute) - `column (attribute: Assoc/Attr, caption: 'label')` - Associated-attribute column (attribute over a reference; bare association name, e.g. `attribute: Order_Customer/Name`; multi-hop `A/B/Attr` supported) - `column (caption: 'label') { ... }` - Custom content column (nested widgets) > **Custom-content columns build correctly** on the default engine (a nested > `actionbutton`/`dynamictext` in `column (caption: …) { … }` — mxbuild-verified, > 0 errors). An earlier CE0463 (column property ordering) was fixed. A row-level > `onclick` (open the NewEdit page on row click) is still a fine alternative for a > row-open affordance. > **Reserved keyword attribute names:** If the attribute name is a reserved MDL keyword (e.g. `Status`, `Type`), quote it. A column takes no name (Mendix stores none), so there is no widget name to choose: > ```sql > column (attribute: "Status", caption: 'Status') > column (attribute: "Type", caption: 'Type') > ``` **Column Properties (non-default only in DESCRIBE output):** | Property | Values | Default | |----------|--------|---------| | `Sortable` | `true`/`false` | `true` (with attribute) | | `Resizable` | `true`/`false` | `true` | | `Draggable` | `true`/`false` | `true` | | `Hidable` | `yes`/`hidden`/`no` | `yes` | | `ColumnWidth` | `autofill`/`autoFit`/`manual` | `autofill` | | `Size` | integer (px) | `1` (when manual) | | `visible` | expression | `true` | | `DynamicCellClass` | expression | (empty) | | `tooltip` | text | (empty) | ### Column Filters (match the attribute's data type) A filter widget must match the column attribute's type, or MxBuild fails with *"The text filter is not compatible with the … data type"*. Do **not** apply `textfilter` to every column — it only works on String attributes. Pick by type: | Attribute type | Filter widget | |----------------|---------------| | String | `textfilter` | | Integer / Long / Decimal / Autonumber | `numberfilter` | | Date and time | `datefilter` | | Enumeration | `dropdownfilter` | | Boolean | *(no filter — every filter widget errors on Boolean)* | ```sql column (attribute: Name) { textfilter f1 } -- String column (attribute: Quantity) { numberfilter f2 } -- Integer/Decimal column (attribute: OrderDate) { datefilter f3 } -- Date and time column (attribute: "Status") { dropdownfilter f4 } -- Enumeration -- Boolean columns: omit the filter entirely ``` **The filter goes inside the column's own braces** — `column c (attribute: Name) { textfilter f1 }`. A `filter { … }` block after the column is the gallery's spelling and is refused as MDL-WIDGET30 on a data grid. That case, and a column over an association (a `dropdownfilter` taking `Association`, `datasource` and `CaptionAttribute`), are in [`reference/column-filters.md`](reference/column-filters.md). **The grid filters itself — do not build a filter bar beside it.** The shape to avoid is a non-persistent filter entity, inputs bound to it, an apply microflow on every change, and an XPath on the grid reading that object back: measured on one generated app, three microflows and 1,100 characters of XPath against five lines, one filter per column. ## NewEdit Page Template Form for creating or editing a single entity. **Requires a page parameter** to receive the object. ```sql mdl 1; create page Module.Entity_NewEdit folder 'OverviewPages' ( params: ( $entity: Module.Entity ), title: 'Edit Entity', layout: Atlas_Core.PopupLayout ) { layoutgrid mainGrid { row { column (desktopwidth: autofill) { dataview dataView1 (datasource: $entity) { -- Input fields for each attribute textbox txtName (label: 'Name', attribute: Name) textbox txtDescription (label: 'Description', attribute: description) datepicker dpDueDate (label: 'Due Date', attribute: DueDate) combobox cbStatus (label: 'Status', attribute: status) footer { actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success) actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } } } }; ``` ### Page Parameter Syntax ```sql create page Module.PageName ( params: ( $ParamName: Module.EntityName ), title: '...', layout: ... ) ``` - Parameter name conventionally matches the entity name (e.g., `$store`, `$Customer`) - The DataView's binding references this parameter (`datasource: $ParamName`) - When calling the page via show page, pass the object by parameter name: `show page Module.PageName(ParamName = $value)` - **Reserved-word parameter names must be quoted in the show page args.** The generic examples above name the parameter `entity`, which is a reserved keyword, so the call quotes it: `show page Module.Entity_NewEdit("entity" = $currentObject)`. A non-reserved name (`$store`, `$Customer`) needs no quotes: `show page Module.Store_NewEdit(store = $currentObject)`. ### NewEdit Page Components 1. **Page Parameter**: `params: ( $entity: Module.Entity )` - Receives the object to edit 2. **Layout**: `Atlas_Core.PopupLayout` - Popup/modal style 3. **DataView**: Container bound to page parameter (`datasource: $entity`) 4. **Input Widgets**: Match entity attributes with `attribute:` property 5. **Footer**: Save and Cancel buttons ## Complete Example: Store Entity ### Step 1: Create the Navigation Snippet First, create a navigation menu snippet that will be shared across all overview pages: ```sql mdl 1; create snippet MdlTemplates.NavigationMenu { layoutgrid navGrid { row { column (desktopwidth: 12) { actionbutton btnStores (caption: 'Stores', action: show page MdlTemplates.Store_Overview) actionbutton btnCars (caption: 'Cars', action: show page MdlTemplates.Car_Overview) } } } }; ``` ### Step 2: Create the Entity ```sql mdl 1; create persistent entity MdlTemplates.Store ( Name: string(200) not null, Location: string(200) ); ``` ### Step 3: Create the Overview Page ```sql mdl 1; create page MdlTemplates.Store_Overview folder 'OverviewPages' ( title: 'Store Overview', layout: Atlas_Core.Atlas_Default ) { layoutgrid mainGrid { row { column (desktopwidth: 12) { snippetcall navMenu (snippet: MdlTemplates.NavigationMenu) } } row { column (desktopwidth: 12) { dynamictext heading (content: 'Stores', rendermode: H2) } } row { column (desktopwidth: 12) { datagrid StoreGrid (datasource: database MdlTemplates.Store) { column (attribute: Name, caption: 'Name') column (attribute: Location, caption: 'Location') } } } } }; ``` ### Store NewEdit Page ```sql mdl 1; create page MdlTemplates.Store_NewEdit folder 'OverviewPages' ( params: ( $store: MdlTemplates.Store ), title: 'Edit Store', layout: Atlas_Core.PopupLayout ) { layoutgrid mainGrid { row { column (desktopwidth: autofill) { dataview dataView1 (datasource: $store) { textbox txtName (label: 'Name', attribute: Name) textbox txtLocation (label: 'Location', attribute: Location) footer { actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success) actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } } } }; ``` ## Complete Example: Car Entity ### Entity Definition ```sql mdl 1; create enumeration MdlTemplates.CarType ( Sedan 'Sedan', SUV 'SUV', Truck 'Truck', Sports 'Sports Car' ); create persistent entity MdlTemplates.Car ( Brand: string(200) not null, model: string(200), Price: decimal, PurchaseYear: integer, PurchaseDate: datetime, CarType: enumeration(MdlTemplates.CarType) ); ``` ### Car NewEdit Page Shows various input widget types: ```sql mdl 1; create page MdlTemplates.Car_NewEdit folder 'OverviewPages' ( params: ( $Car: MdlTemplates.Car ), title: 'Edit Car', layout: Atlas_Core.PopupLayout ) { layoutgrid mainGrid { row { column (desktopwidth: autofill) { dataview dataView1 (datasource: $Car) { textbox txtBrand (label: 'Brand', attribute: Brand) textbox txtModel (label: 'Model', attribute: model) textbox txtPrice (label: 'Price', attribute: Price) textbox txtYear (label: 'Purchase year', attribute: PurchaseYear) datepicker dpDate (label: 'Purchase date', attribute: PurchaseDate) radiobuttons rbType (label: 'Car type', attribute: CarType) footer { actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success) actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } } } }; ``` ## Widget Selection Guide Choose input widgets based on attribute type: | Attribute Type | Widget | Example | |----------------|--------|---------| | String | `textbox` | Name, Description | | String (long) | `textarea` | Comments, Notes | | Integer, Long, Decimal | `textbox` | Price, Quantity | | Boolean | `checkbox` or `radiobuttons` | IsActive, IsPublished | | DateTime | `datepicker` | DueDate, OrderDate | | Enumeration | `combobox` or `radiobuttons` | Status, Type | | Association (reference) | `combobox` with DataSource | Category, Owner | **Note:** `dropdown` is deprecated. Use `combobox` for enumeration attributes. **ComboBox modes:** - Enum mode: `combobox cb (label: 'status', attribute: status)` - Association mode: `combobox cb (label: 'Customer', attribute: Order_Customer, datasource: database MyModule.Customer, CaptionAttribute: Name)` **Reserved Attribute Names:** Do not use `CreatedDate`, `ChangedDate`, `owner`, `ChangedBy` as attribute names - these are system attributes automatically added to all entities. ## Naming Conventions | Item | Convention | Example | |------|------------|---------| | Navigation Snippet | `NavigationMenu` | `MdlTemplates.NavigationMenu` | | Overview Page | `Entity_Overview` | `Customer_Overview` | | NewEdit Page | `Entity_NewEdit` | `Customer_NewEdit` | | Folder | `OverviewPages` | — | | DataView | `dataView1` or `dv{entity}` | `dvCustomer` | | DataGrid | `dataGrid1` or `dg{entity}` | `dgCustomer` | | SnippetCall | `navMenu` or descriptive name | `navMenu`, `headerSnippet` | ## Button Styles | Style | Use Case | Color | |-------|----------|-------| | `success` | Save, Confirm | Green | | `default` | Cancel, Back | Gray | | `primary` | Primary action | Blue | | `danger` | Delete | Red | | `warning` | Caution actions | Yellow | ## Folder Organization ``` module/ ├── snippets/ │ └── NavigationMenu ├── OverviewPages/ │ ├── Customer_Overview │ ├── Customer_NewEdit │ ├── Order_Overview │ ├── Order_NewEdit │ └── ... ├── microflows/ └── entities/ ``` ## Parameterized Snippets Snippets can accept parameters to display context-specific data. **A snippet parameter must be an entity.** A primitive one (`params: ( $Label: String )`) is refused as **MDL087**, because Mendix rejects it with **CE0046** *"Invalid data type 'String'."* — a *page* parameter may be a primitive, a snippet parameter may not. To parameterise a snippet on a value, keep the primitive on the calling page's parameters, or pass an object and read the member inside the snippet. ```sql mdl 1; -- Create a snippet with a parameter create snippet Module.CustomerDetails ( params: ( $Customer: Module.Customer ) ) { layoutgrid detailsGrid { row { column (desktopwidth: 12) { dynamictext heading (content: 'Customer Details', rendermode: H3) } } } }; ``` Use the snippet with parameter passing, inside a page body: ```sql snippetcall customerDetails (snippet: Module.CustomerDetails, params: (Customer = $Customer)) ``` ## Entity Menu Snippets with NavigationList For entity-specific action menus (Edit, Delete, etc.), use the `navigationlist` widget: ```sql mdl 1; create snippet Module.Entity_Menu ( params: ( $EntityParameter: Module.Entity ) ) { navigationlist EntityMenuNav { item itemEdit (caption: 'Edit', action: show page Module.Entity_NewEdit("entity" = $EntityParameter)) item itemDelete (caption: 'Delete', action: delete) item itemBack (caption: 'Back', action: close page) } }; ``` ### NavigationList Syntax ```sql navigationlist widgetName { item itemName (caption: 'Caption', action: ACTION_TYPE) } ``` **Supported Actions:** - `action: save changes` - Save changes - `action: cancel changes` - Cancel changes - `action: close page` - Close current page - `action: delete` - Delete object - `action: call microflow Module.MicroflowName` - Call microflow - `action: call microflow Module.MicroflowName(Param = $value)` - Call microflow with parameters - `action: show page Module.PageName` - Navigate to page - `action: show page Module.PageName(Param = $value)` - Navigate with parameters - **A `show page` argument must be the context object, and there has to BE one.** Mendix takes the page argument from the enclosing data widget, so the only spellings that mean anything are `$currentObject` or the name of the variable that widget is bound to (`datasource: $Customer` → `(Customer = $Customer)` is fine). Naming any other variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently opened the page with the context object anyway. - **Outside a data widget the same rule leaves nothing at all**, so a button sitting on the page itself (or in a plain `container`/`layoutgrid`) may pass **no** argument — not a page parameter, not `$currentObject`, not a literal. There is no context object there for Mendix to infer, and the page opens with nothing: mxbuild reports **CE1571** per parameter of the target page, and a page whose parameters are optional would simply show the wrong data. MDL-PAGEARG01 refuses that too (mendixlabs/mxcli#1029). To open a parameterised page from such a button, call a microflow that does `show page Module.Page(Param = $value)` — that path wires the arguments properly. ## Handling Circular Dependencies When a navigation snippet references pages (via `show page`) and those pages reference the snippet (via `snippetcall`), you have a circular dependency. Use the **placeholder pattern**: ### Creation Order 1. **Create placeholder snippet first** (before pages) 2. **Create all pages** (which reference the snippet via SNIPPETCALL) 3. **Replace snippet with full content** (which can now reference existing pages) ### Example Pattern ```sql mdl 1; -- Step 1: Create placeholder snippet (pages can reference this) create snippet Module.NavigationMenu { layoutgrid navGrid { row { column (desktopwidth: 12) { dynamictext loading (content: 'Loading...') } } } }; -- Step 2: Create all pages (they reference the snippet via SNIPPETCALL) create page Module.Customer_NewEdit ( params: ( $Customer: Module.Customer ), title: 'Edit Customer', layout: Atlas_Core.PopupLayout ) { -- ... page content with SNIPPETCALL navMenu (Snippet: Module.NavigationMenu) }; create page Module.Customer_Overview ( title: 'Customer Overview', layout: Atlas_Core.Atlas_Default ) { -- ... page content with SNIPPETCALL navMenu (Snippet: Module.NavigationMenu) }; -- Step 3: Fill in the snippet with real content (pages now exist) -- Use CREATE OR MODIFY (preserves the snippet's ID → page bindings stay valid) -- Do NOT use CREATE OR REPLACE — that would assign a new ID and break existing page references create or modify snippet Module.NavigationMenu { layoutgrid navGrid { row { column (desktopwidth: 12) { actionbutton btnCustomers (caption: 'Customers', action: show page Module.Customer_Overview) } } } }; ``` ### Key Points - The placeholder snippet must exist before pages are created (for `snippetcall` to resolve) - Use `create or modify snippet` for the fill-in step — it preserves the snippet's UUID so pages that already reference it remain valid - **Do not use `create or replace snippet`** — that deletes the placeholder and creates a fresh UUID, silently breaking every page that references the old one - Page references in the final snippet resolve correctly because pages already exist See [Resolve Forward References](../resolve-forward-references/SKILL.md) for the full pattern including page→page and microflow→page cases, declaration ordering rules, and the choice between `CREATE OR MODIFY` and `ALTER SNIPPET`. ## Related Skills - [Create Page](../create-page/SKILL.md) - Basic page creation syntax - [ALTER PAGE/SNIPPET](../alter-page/SKILL.md) - Modify existing pages/snippets in-place (SET, INSERT, DROP, REPLACE) - [Master-Detail Pages](../master-detail-pages/SKILL.md) - Selection binding pattern - [Resolve Forward References](../resolve-forward-references/SKILL.md) - Placeholder pattern, declaration ordering ## Snippet Commands Reference | Command | Description | |---------|-------------| | `list snippets [in module]` | List all snippets | | `show snippet Module.Name` | Show snippet summary | | `describe snippet Module.Name` | Show snippet MDL source | | `create snippet Module.Name { ... }` | Create a new snippet | | `create or modify snippet Module.Name { ... }` | Create or update snippet | | `alter snippet Module.Name { ... }` | Modify snippet widgets in-place | | `drop snippet Module.Name` | Delete a snippet |