Back to skills
SKILL.md
Overview Pages
ASecurityThe 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.
- 128 stars
- 0 votes
- 0 copies
- 1 view
- Added September 26, 2026
Works with
Security analysis
100/100Pro scans all 2 files and shows the line behind each finding
npx -y skills add mendixlabs/mxcli --skill overview-pages --agent claude-codeAre you the author of Overview Pages?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mendixlabs-overview-pages)---
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 |
Files in this skill
- SKILL.md
- reference/column-filters.md
Attribution
Comments
Loading comments…