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.
Per-mode rendering (SubView)
Section titled “Per-mode rendering (SubView)”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.
Selected-value labels
Section titled “Selected-value labels”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.
Persistence
Section titled “Persistence”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>Edit mode (pre-fill + persist)
Section titled “Edit mode (pre-fill + persist)”A cart maps to no real column, so to make it work in edit the same way as create:
- 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 viaColumns:<Sql …> SELECT s.Id, …,(SELECT si.ProductId, si.Quantity, si.UnitPrice FROM Sales.SaleItems siWHERE si.SaleId = s.Id AND si.IsDeleted = 0 FOR JSON PATH) AS LinesFROM Sales.Sales s … </Sql><Columns> <Column Name="Lines" Hide="true" Export="false" /> </Columns> - Persist — mirror the
OPENJSONexpansion in<EditSql>: soft-delete the current child rows for@Id, then re-insert from@Lines(same block asInsertSql, keyed on@Id). - 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). WithoutSubView, 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.