Acumatica · Modernui

Acumatica Modern UI Master-Detail Pattern

Acumatica Modern UI Master-Detail Pattern is one of those Acumatica topics that is both obvious and subtle. The Modern UI is the default since 2025 R2, the Mobile Framework is the.

John Kihiu12 min read

Master-detail is the single most common screen shape in Acumatica — a document header with a grid of lines underneath — and it's also where Modern UI's view-factory model shows its clearest advantage over the classic ASPX approach: the parent-child relationship between header and lines is declared once, in the TypeScript screen class, rather than wired together across a DataSource control and nested grid markup. Here's how it actually fits together, building a simple purchase-request screen as the example.

createSingle for the header, createCollection for the lines

Modern UI's view factories mirror the shape of the underlying graph's PXSelect views almost one-to-one. A single-record view (the document header) uses createSingle; a related grid of child rows uses createCollection, and the parent-child linkage is expressed through the graph's own view hierarchy, not reconstructed in the frontend:

TYPESCRIPT
@graphInfo({ graphType: "MyNamespace.Graphs.PurchaseRequestEntry", primaryView: "Document" })
export class PurchaseRequestScreen extends PXScreen {
  Document = createSingle(PurchaseRequestHeader);
  Lines = createCollection(PurchaseRequestLine);
}

class PurchaseRequestHeader extends PXView {
  RequestNbr = createField<PXFieldState<string>>({});
  RequestedBy = createField<PXFieldState<string>>({});
  RequestTotal = createField<PXFieldState<number>>({ displayFormat: "0.00" });
}

class PurchaseRequestLine extends PXView {
  InventoryID = createField<PXFieldState<string>>({ required: true });
  Qty = createField<PXFieldState<number>>({ required: true });
  UnitCost = createField<PXFieldState<number>>({});
  LineTotal = createField<PXFieldState<number>>({ displayFormat: "0.00" });
}

Notice there's no explicit "Lines belongs to Document" wiring anywhere in this TypeScript — that relationship is defined server-side, in the graph's PXSelect view declaration for Lines being conditioned on the current Document row, exactly the same way it would be for a classic screen. The frontend view factories mirror whatever shape the graph already declares; they don't reimplement the relationship.

Layout: qp-form for the header, qp-grid for the detail

The screen's layout markup composes the standard layout web components — qp-form for the header's field grid, qp-grid bound to the collection view for the lines — and the framework handles the row-selection-drives-header-context behavior (clicking a line doesn't change the header context here, since Lines aren't independently navigable the way, say, a Sales Order's shipments might be) automatically based on which view each component is bound to.

Where it gets more interesting is nested master-detail — a document with lines, and each line with its own sub-collection (say, serial numbers per line). That's a third createCollection, conditioned server-side on the currently selected line, and the frontend layout nests a second grid or a side-panel bound to that third view. The pattern repeats at whatever depth the graph's view hierarchy actually has; Modern UI doesn't impose an arbitrary nesting limit, though in practice more than two levels of nested detail is a sign the screen might be trying to do too much in one place.

Header totals recalculating from line changes

A frequent master-detail requirement — the header's total updates live as lines are added/edited — is handled the same way it always has been: server-side, in the graph's line-level event handlers (typically RowUpdated or RowDeleted on the Lines view triggering a header field recalculation), not reimplemented as client-side arithmetic in TypeScript. The Modern UI screen simply re-renders the header view after the server round trip that follows a line edit, picking up whatever the graph computed:

C#
// Still server-side, unchanged in shape from a classic-UI equivalent
protected virtual void _(Events.RowUpdated<PurchaseRequestLine> e)
{
    var header = Document.Current;
    if (header == null) return;
    header.RequestTotal = Lines.Select().RowCast<PurchaseRequestLine>()
        .Sum(l => (l.Qty ?? 0) * (l.UnitCost ?? 0));
    Document.Cache.Update(header);
}
Line-level commitChanges controls how "live" the total feels

If Qty or UnitCost fields on the line don't have commitChanges: true set in their TypeScript field definitions, edits to those fields won't trigger the server round trip that recalculates the header total until the user tabs to a field that does commit, or explicitly saves — which can look like a bug ("the total isn't updating") when it's actually a field configuration choice. Decide deliberately which line fields should commit immediately versus batch with the rest of the row's edits; committing every keystroke on every field is chattier than most screens need.

Adding and removing detail rows

Row insertion and deletion on the collection view map to the graph's standard PXCache insert/delete operations, invoked from the grid's built-in add/delete row actions or from a custom PXAction exposed the same way described for other Modern UI action patterns. Nothing here differs conceptually from classic UI grids — the grid component's row lifecycle is a thin client-side reflection of the same cache-tracked row states (Inserted/Updated/Deleted) that have always driven Acumatica's data model.

Wrapping up

Modern UI master-detail is createSingle for the header and createCollection for each level of detail, mirroring whatever view hierarchy the graph already declares server-side — the parent-child relationship, recalculation logic, and row lifecycle all still live in the graph exactly as they would for a classic screen. The frontend's job is composing qp-form and qp-grid against those views and deciding, deliberately, which fields commit immediately versus batch — that decision is what determines whether a master-detail screen feels responsive or laggy, far more than anything about the framework itself.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.