Skip to content

CartTable

A <CartTable> is an inline, editable grid of line items inside a form — an order’s lines, an invoice’s items. It is an editor field whose value submits as a JSON array, and it renders per-mode (editable in Create/Edit, read-only in View).

<CartTable Name="Lines" SubView="SaleItem" RowStyle="Grid" AllowAdd="true" AllowEdit="true" AllowRemove="true"
ShowRowNumbers="true" ShowHeader="true" AddButtonText="Add" RemoveButtonText="Remove" MaxRows="50">
<Columns>
<Select Name="ProductId" Label="Product" Width="5"><DataSet Type="sql">SELECT Id AS Value, Name AS Label FROM Sales.Products</DataSet></Select>
<Number Name="Quantity" Label="Qty" Width="3" />
</Columns>
</CartTable>

RowStyle is Grid or Stacked. Each <Columns> child is itself a full editor field — including a <Select> with ControlType="ModalSelector", its own <DataSet>, <Columns>, and <ExtraMappings> (picking a row autofills sibling cells in the same row, e.g. Product → Unit Price). Alternatively <CartTable Name="…" TableName="…" /> binds a server-side “Genie” cart table.

To total the lines in a header field, use the sumLines(...) helper in a ValueExpression.

SubView links the cart to the <SubConfig> sub-view that holds the same rows. It drives per-mode rendering: Create/Edit show the editable cart; View renders that sub-view read-only (permission-gated) in the cart’s place — so you never need a separate <Sub> for the line items, and the view never shows raw JSON. When SubView is omitted the form falls back to a sub-view named after the cart field (and, for a TableName cart, to that table); if none resolves, View renders a plain read-only cart table. A referenced (TableName) cart always renders as the sub-view table — editable in Create/Edit, read-only in View.

A dataset-backed column shows the label of each stored selection in edit (and the read-only fallback), even when the option list isn’t fully loaded. The cart sends the stored keys to the field-dataset endpoint as CurrentValues and the backend returns just those rows (works for SQL / Options / CSV / Model datasets). For very large or search-only SQL datasets you can narrow this server-side with WHERE Value IN (SELECT value FROM STRING_SPLIT(@CurrentValues, ',')); otherwise no SQL change is needed.

A local cart is an in-memory field whose value submits as a JSON array under its own name (@{Name}, e.g. @Lines). Persist it in <InsertSql> by expanding that JSON into child rows (SQL Server OPENJSON); capture the new parent id first and end by returning it:

<InsertSql>
INSERT INTO Sales.Sales (InvoiceNumber, CustomerId, TotalAmount, …) VALUES (@InvoiceNumber, @CustomerId, @TotalAmount, …);
DECLARE @NewId INT = CAST(SCOPE_IDENTITY() AS INT);
IF (@Lines IS NOT NULL AND ISJSON(@Lines) = 1)
INSERT INTO Sales.SaleItems (SaleId, ProductId, Quantity, UnitPrice, LineTotal, …)
SELECT @NewId, TRY_CAST(j.ProductId AS INT), TRY_CAST(j.Quantity AS INT), TRY_CAST(j.UnitPrice AS DECIMAL(18,2)),
TRY_CAST(j.Quantity AS INT) * TRY_CAST(j.UnitPrice AS DECIMAL(18,2)), …
FROM OPENJSON(@Lines) WITH (ProductId NVARCHAR(50) '$.ProductId', Quantity NVARCHAR(50) '$.Quantity', UnitPrice NVARCHAR(50) '$.UnitPrice') AS j
WHERE TRY_CAST(j.ProductId AS INT) IS NOT NULL;
SELECT @NewId;
</InsertSql>

A cart maps to no real column, so to make it work in edit the same way as create:

  1. Pre-fill — add a JSON-array column to the main <Sql>, aliased to the cart field name, with keys matching the cart column names. The form-values load wraps <Sql> (… WHERE Id=@Id), so the cart parses this JSON to seed its rows. Hide it in the grid via Columns:
    <Sql …> SELECT s.Id, …,
    (SELECT si.ProductId, si.Quantity, si.UnitPrice FROM Sales.SaleItems si
    WHERE si.SaleId = s.Id AND si.IsDeleted = 0 FOR JSON PATH) AS Lines
    FROM Sales.Sales s … </Sql>
    <Columns> <Column Name="Lines" Hide="true" Export="false" /> </Columns>
  2. Persist — mirror the OPENJSON expansion in <EditSql>: soft-delete the current child rows for @Id, then re-insert from @Lines (same block as InsertSql, keyed on @Id).
  3. View mode — set SubView="…" on the <CartTable> (pointing at the <SubConfig> sub-view for the same rows). The cart then drives all three modes itself: editable cart in Create/Edit, the read-only sub-view in View. Place a single <Item Name="{cart}" /> in the layout — no separate <Sub> is needed (a standalone <Sub> here would duplicate the cart in edit). Without SubView, View falls back to a read-only cart table. Use a standalone <Sub> only for child grids that aren’t backed by a cart.

Worked example: sample/Inventory/models/views/PurchaseOrders.view.xml — a local <CartTable> (JSON @Lines) with a ModalSelector product column that autofills the line price via <ExtraMappings>, a per-line LineTotal ValueExpression, a header TotalAmount using sumLines(...), and SubView="PurchaseOrderLines" for read-only View mode.