overview-pages · git:20260922.2adda8b · 2026-09-22 · sha256 a6401d3415e2ccd1

overview-pages git:20260922.2adda8bA

Immutable. This exact content is served forever at /api/v1/blob/a6401d3415e2ccd1.

---
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
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: 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
create page Module.Entity_Overview
(
  title: 'Entity Overview',
  layout: Atlas_Core.Atlas_Default,
  folder: 'OverviewPages'
)
{
  layoutgrid mainGrid {
    row row1 {
      column colNav (desktopwidth: 2) {
        snippetcall navMenu (snippet: Module.Entity_Menu)
      }
      column colContent (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 colName (attribute: Name, caption: 'Name') {
            textfilter textFilter1
          }
          column colDescription (attribute: Description, caption: 'Description') {
            textfilter textFilter2
          }
          column colActions (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 colName (attribute: Name, caption: 'Name') {
    textfilter textFilter1
  }
  column colActions (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 colName (attribute: attribute, caption: 'label')` - Attribute column (own-entity attribute)
- `column colName (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 colName (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 column names:** If the attribute name is a reserved MDL keyword (e.g. `Status`, `Type`), you must quote it and use a distinct column widget name:
> ```sql
> column colStatus (attribute: "Status", caption: 'Status')
> column colType   (attribute: "Type",   caption: 'Type')
> ```
> Using `COLUMN Status (attribute: Status)` fails silently — the column won't sort or filter correctly because `Status` is parsed as a keyword. Always prefix the widget name (`colStatus`) when the attribute name is reserved.

**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 colName   (attribute: Name)      { textfilter f1 }      -- String
column colQty    (attribute: Quantity)  { numberfilter f2 }    -- Integer/Decimal
column colDate   (attribute: OrderDate) { datefilter f3 }      -- Date and time
column colStatus (attribute: "Status")  { dropdownfilter f4 }  -- Enumeration
-- Boolean columns: omit the filter entirely
```

**The filter goes inside the column's own braces.** A `filter { … }` block is
the GALLERY spelling of a different thing — the widget-wide filter bar, which a
data grid calls `controlbar`:

```sql
-- ✅ data grid: per-column filter, inside the column
datagrid dg (...) { column colName (attribute: Name) { textfilter f1 } }

-- ✅ gallery: the widget-wide filter bar, which the gallery calls `filter`
gallery g (...) { filter f { textfilter f1 } }

-- ❌ the gallery form on a data grid — MDL-WIDGET30
datagrid dg (...) { column colName (attribute: Name) filter f { textfilter f1 } }
```

That last line is worth reading twice: it is not a column with a filter block.
A widget is `type name (props) { body }`, so with the `filter` *outside* the
column's braces it parses as a column with **no body** followed by a separate
`filter` widget — which the grid has nowhere to put. It used to be dropped on
write with no diagnostic, so `DESCRIBE PAGE` showing a filterless column was the
only symptom; it is now refused at check and exec time.

**A column over an association is filtered by the associated objects.** The column
shows a value from the other side (`attribute: Order_Customer/Name`); the filter takes
the reference, the option list and what an option shows — all three, or it is refused:

```sql
column colCustomer (attribute: Order_Customer/Name, caption: 'Customer') {
  dropdownfilter fltCustomer (
    Association: Sales.Order_Customer,    -- the reference on the grid's entity
    datasource: database Sales.Customer,  -- the option list
    CaptionAttribute: Name                -- what each option shows
  )
}
```

A `datefilter` compares one date; `FilterType: between` makes the column a range.

**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
create page Module.Entity_NewEdit
(
  params: { $entity: Module.Entity },
  title: 'Edit Entity',
  layout: Atlas_Core.PopupLayout,
  folder: 'OverviewPages'
)
{
  layoutgrid mainGrid {
    row row1 {
      column col1 (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 footer1 {
            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
create snippet MdlTemplates.NavigationMenu
{
  layoutgrid navGrid {
    row row1 {
      column col1 (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
create persistent entity MdlTemplates.Store (
  Name: string(200) not null,
  Location: string(200)
);
```

### Step 3: Create the Overview Page

```sql
create page MdlTemplates.Store_Overview
(
  title: 'Store Overview',
  layout: Atlas_Core.Atlas_Default,
  folder: 'OverviewPages'
)
{
  layoutgrid mainGrid {
    row row1 {
      column col1 (desktopwidth: 12) {
        snippetcall navMenu (snippet: MdlTemplates.NavigationMenu)
      }
    }
    row row2 {
      column col2 (desktopwidth: 12) {
        dynamictext heading (content: 'Stores', rendermode: H2)
      }
    }
    row row3 {
      column col3 (desktopwidth: 12) {
        datagrid StoreGrid (datasource: database MdlTemplates.Store) {
          column colName (attribute: Name, caption: 'Name')
          column colLocation (attribute: Location, caption: 'Location')
        }
      }
    }
  }
}
```

### Store NewEdit Page

```sql
create page MdlTemplates.Store_NewEdit
(
  params: { $store: MdlTemplates.Store },
  title: 'Edit Store',
  layout: Atlas_Core.PopupLayout,
  folder: 'OverviewPages'
)
{
  layoutgrid mainGrid {
    row row1 {
      column col1 (desktopwidth: autofill) {
        dataview dataView1 (datasource: $store) {
          textbox txtName (label: 'Name', attribute: Name)
          textbox txtLocation (label: 'Location', attribute: Location)

          footer footer1 {
            actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: success)
            actionbutton btnCancel (caption: 'Cancel', action: cancel_changes)
          }
        }
      }
    }
  }
}
```

## Complete Example: Car Entity

### Entity Definition

```sql
create persistent entity MdlTemplates.Car (
  Brand: string(200) not null,
  model: string(200),
  Price: decimal,
  PurchaseYear: integer,
  PurchaseDate: datetime,
  CarType: enumeration(MdlTemplates.CarType)
);

create enumeration MdlTemplates.CarType (
  Sedan 'Sedan',
  SUV 'SUV',
  Truck 'Truck',
  Sports 'Sports Car'
);
```

### Car NewEdit Page

Shows various input widget types:

```sql
create page MdlTemplates.Car_NewEdit
(
  params: { $Car: MdlTemplates.Car },
  title: 'Edit Car',
  layout: Atlas_Core.PopupLayout,
  folder: 'OverviewPages'
)
{
  layoutgrid mainGrid {
    row row1 {
      column col1 (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 footer1 {
            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:

```sql
-- Create a snippet with a parameter
create snippet Module.CustomerDetails
(
  params: { $Customer: Module.Customer }
)
{
  layoutgrid detailsGrid {
    row row1 {
      column col1 (desktopwidth: 12) {
        dynamictext heading (content: 'Customer Details', rendermode: H3)
      }
    }
  }
}

-- Use the snippet with parameter passing
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
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: microflow Module.MicroflowName` - Call microflow
- `action: 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
-- Step 1: Create placeholder snippet (pages can reference this)
create snippet Module.NavigationMenu
{
  layoutgrid navGrid {
    row row1 {
      column col1 (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 row1 {
      column col1 (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 |
|---------|-------------|
| `show 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 replace snippet Module.Name { ... }` | Create or update snippet |
| `alter snippet Module.Name { ... }` | Modify snippet widgets in-place |
| `drop snippet Module.Name` | Delete a snippet |