overview-pages

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.

SKILL.md

GitHub
<!-- skill-index: generated by scripts/skill-index.py; make check-skill-index -->

Find it fast

sed -n '<from>,<to>p' SKILL.md one section rather than reading the file; ./mxcli syntax <topic> answers how do I write this for a fraction of either.

lines section
33-40 Overview
41-48 Pattern Summary
49-89 Navigation Menu Snippet
90-257 Overview Page Template
258-315 NewEdit Page Template
316-408 Complete Example: Store Entity
409-466 Complete Example: Car Entity
467-488 Widget Selection Guide
489-500 Naming Conventions
501-510 Button Styles
511-526 Folder Organization
527-559 Parameterized Snippets
560-614 Entity Menu Snippets with NavigationList
615-684 Handling Circular Dependencies
685-691 Related Skills
692-702 Snippet Commands Reference
<!-- /skill-index -->

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:

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

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:

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 │                                  │
│ └────────┴──────────────────────────────────┤
└─────────────────────────────────────────────┘
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:

-- 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

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:

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)
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.

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.

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

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:

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

mdl 1;
create persistent entity MdlTemplates.Store (
  Name: string(200) not null,
  Location: string(200)
);

Step 3: Create the Overview Page

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

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

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:

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.

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:

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:

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

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.
  • The exception is a parameter of the page or snippet itself. (Item = $Item) where $Item is the document's own entity parameter is bound BY NAME — Studio Pro stores an explicit mapping for it — so it works anywhere, including on a button outside every data widget (mendixlabs/mxcli#1403).
  • Outside a data widget nothing else is left, so a button sitting on the page itself (or in a plain container/layoutgrid) may pass no other argument — not $currentObject, not a literal, not a variable that is no parameter of the document. 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. MDL-PAGEARG01 refuses that too (mendixlabs/mxcli#1029). To open a parameterised page with anything else from such a button, call a microflow that does show page Module.Page(Param = $value).

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

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)
-- (CREATE OR REPLACE is its deprecated spelling, MDL-DEPR001 — same meaning)
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
  • create or replace snippet is the deprecated spelling of the same statement (MDL-DEPR001) — it keeps the UUID too, but write or modify
  • Page references in the final snippet resolve correctly because pages already exist

See Resolve Forward References 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

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