Skip to content
Back to skills

Overview Pages

ASecurity

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.

  • 128 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 26, 2026
ai-agentsgosqlexpressdatabase

Works with

  • cli

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add mendixlabs/mxcli --skill overview-pages --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Overview Pages?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Overview Pages
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mendixlabs-overview-pages/badge)](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.

Download with Pro
SKILL.md
---
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.md23.5 KB
  • reference/column-filters.md1.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…