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.
Pro scans all 2 files and shows the line behind each finding
Scanned 10/4/2026
npx -y skills add mendixlabs/mxcli --skill overview-pages --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Overview Pages?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mendixlabs-overview-pages)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
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 |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!