ZuloOne
English
Sign in
Menu

📄 Print forms

Updated

A print form is the MetaPrintForm entity (Name + Caption), a data script, and — for ZuloReport — LayoutJson. Short recipe: Print a form. ZuloReport does not fill from one SlimTable. ZuloReportForm.PrintAsync writes a catalog of cells and tables, and the layout binds to those names. Html and Xlsx still use PrintFormBase.GetDataTemplate / GetDataAsync (SlimTable / SlimRow). The list is /printforms (the Reporting ribbon), the Print forms block on a document subtype's card (the Subtypes tab, above the commands) and the Print forms tab on a dictionary. The list shows Caption (falling back to Name).

The workspace: Documents/<Doc>/PrintForms/<Name>.printform.json or Dictionaries/<Dict>/PrintForms/<Name>.printform.json (the form + its bindings

  • LayoutJson), with the data script alongside (*.script.json + *.cs). Without those files the form lives in the database only and is lost when the model is shipped or imported.

There are three engines. ZuloReport (Zr) is the canonical name for the banded JSON layout, HTML renderer, and visual designer (Tool Box, canvas, property window, Preview). Zr is the only accepted engine value for this layout. Html and Xlsx keep their existing behavior.

LayoutJson uses control names such as ZRLabel and ZRBarCode directly. The parser accepts only ZR* controls. Report scripts inherit from ZuloReport and use ZR* control names. The Zr engine name is required. Implementation classes and source files use the Zr* prefix.

← Developer wiki

The engines#

Engine Data Layout Rendering
Html GetDataAsync → a table, otherwise Render none GET /api/documents/{type}/{id}/print?formId=
Xlsx the header + table parts an Excel resource {Field} / {{Part.Field}} GET /api/metadata/print-forms/{id}/render/{documentId}
Zr ZuloReportForm.PrintAsync writes catalog cells and table rows LayoutJson the same /print?formId= or …/preview/{documentId}

Creating a form (ZuloReport/Html) immediately creates a PrintForm script and writes ScriptMetaId. A Zr script subclasses ZuloReportForm and overrides PrintAsync. Html still subclasses PrintFormBase. The Zr Field List is the layout catalog (cells / tables); when the catalog is empty it uses the document or dictionary schema. Html still uses GetDataTemplate, then that same schema.

Inside a print script (PrintAsync or GetDataAsync), context.GetService<T>() resolves exactly as in an event handler: the script deny-list first, then platform services from DI, then model service contracts (IPricingService and the like). The data script is compiled once per source revision, not once per print.

When a Zr script fails to compile, is only a PrintFormBase, or PrintAsync throws, print does not fall back to GetDataAsync or the raw record. Preview shows the reason in the yellow strip, including override PrintAsync on ZuloReportForm when the class is not a ZuloReportForm. Html still fills from GetDataAsync. A fallback would look successful: the raw record has fields, every catalog binding would stay empty, and the sheet would print blanks with no error.

The same cut is why a Body section's table and a child ZRTable.table are exclusive (SectionTableXor). The section binding repeats the whole section once per catalog row (a row template of labels, no inner grid). The control binding expands one grid inside a section that prints once. Setting both reprints every grid on every line.

DocumentTypeMetaId is the data source for a document script (the first selected document type, or the parent of the first selected subtype, otherwise the stored value). A form created from a dictionary has DocumentTypeMetaId = Guid.Empty; such a form can only be created with a Dictionary binding. Where the Print button shows the form is decided by the MetaPrintFormBindings (TargetKind = DocumentType / DocumentSubtype / Dictionary, Scope = Record / List). The right side of the form card contains the dictionary list and the document tree: a document type is a group header with no checkboxes, and the Record / List checkboxes belong to subtypes only. A type list shows the form when List is selected on at least one of its subtypes. An empty set of bindings = the form is visible on its own document type (list and card). A dictionary with no explicit binding is not shown. A new form created from a dictionary's tab selects Record + List on that dictionary immediately. A new form created from a subtype's card selects Record + List on that subtype only; an existing form can be bound or unbound, the same way a command is.

The print dialog (a document list, a document card, a dictionary's list and card): System printer (window.print()) plus any print-agent printer the current user may use. Preview stays in the browser. Print on an agent printer enqueues a PDF of the picked form (see Print agents). Xlsx downloads. A dictionary: GET /api/metadata/print-forms/{id}/print?kind=Dictionary&objectId=&recordId=.

Document-list batch printing#

The journal sends its selected recordIds, or the rows currently shown when nothing is selected. For a ZuloReport or Html form, printOpen.ts requests the normal single-record HTML for each id, then combines those pages in one browser document. Each source document begins on a new sheet; duplicate styles and inline renderer scripts are included once, so ZR page-number controls count the combined pages. Copies repeats the complete batch.

The batch is browser HTML printing only. Server PDF download, Xlsx rendering, and a print-agent queue job each have a one-record contract; the dialog refuses a multi-record request for those actions instead of silently printing only the first selected row. There is no multi-record PDF/Xlsx payload and no queue job that records a set of source records.

GET /api/metadata/print-forms/available?kind=&id=&scope=&subtypeId= — the list for the dialog.

GET /print with no formId: the first MetaPrintForm by DisplayOrder. When there is none, or it is not ZuloReport — the Html script (a subtype outranks a type). Xlsx with no formId redirects to render.

Dataset#

ZuloReport catalog#

One SlimTable cannot be the fill. A form needs values that print once (the number, the customer, a total) and several tables that grow on their own (lines, payments). Putting the header into every row, then pointing one Detail at a single member, is why a second grid on the same sheet had nowhere to live.

PrintAsync writes the catalog. The designer binds visuals to those names. The engine repeats after the script returns; the script does not print each line.

Report["Number"] = invoice.ID;
Report.Table("Lines").Add(new { LineNo = 1, Item = "Nail", Qty = 2m });

Report["Number"] is catalog cell Number. Table("Lines").Add appends a row whose property names are columns. One unbound Body may hold several ZRTable controls, each bound to its own catalog table. A grid row is Header, Data, or Footer. Header and Footer bind to catalog cells (or a fixed caption). Data binds to columns of that table and is cloned once per Add. An empty table prints Header and Footer only. A ZRTable with no catalog table is a header block: Header (and Footer) rows bound to cells, no Data row.

There is no section-editor modal. Placement, pages, and the section's catalog table are properties in the right pane. A section is defined by where it prints, not by a DevExpress band kind: placement (TopMargin, PageHeader = top of the page, Body = in the flow, PageFooter = bottom of the page, BottomMargin) and, at the top or bottom of the page, pages (All, First, Last, FirstAndLast, NotFirst, NotLast). The canvas header of a section shows its name and that place (Totals [Bottom of page · Last]); kind stays in the JSON only so DevExpress layouts still open. Add section (canvas band menu and Explorer) offers the places: top of every page, top of the first page, body, bottom of every page, bottom of the last page, page numbers. A top or bottom section with multiPageOnly: true (Only when several pages) is left out when the document fits on one sheet; the Page numbers preset is that section with a ZRPageInfo (NumberOfTotal, "1 / 3"). Its height is still reserved while pages are planned, so hiding it never changes the page count.

A layout written by the assistant (set_print_form_layout) gets every section exactly as tall as its controls (ZrLayoutFit): the gap under the last control goes, a control hanging out of its section is taken in. Margins, vertical columns and sections with no controls keep their height; only heightMm changes. Layouts generated outside the assistant (a workspace generator script) should do the same before they are committed.

Html and Xlsx#

The script builds the main SlimTable (its rows are SlimRow): the header repeats in every row; nested tables (NestedTables) become parts. A document header stored in a dictionary must be added through AddRow: a plain Add unrolls an IDictionary into a set of pairs.

public override SlimTable GetDataTemplate()
    => new SlimTable(new { Firm = "", Qty = 0L });

public override async Task<SlimTable> GetDataAsync(PrintFormContext context)
{
    var table = new SlimTable("Report");
    table.Add(new { Firm = "ACME", Qty = 2L });
    return table;
}

With no script, the source is the same one Xlsx FillSheet uses: the document header

  • table parts. For a dictionary it is the record's fields (header) and parts.Records (the same fields, for a list); the dictionary's lists (MetaDictionaryLists) are separate parts.

Element scripts (the Scripts tab)#

This is not the catalog fill and not GetDataAsync. One C# scriptsSource per layout, plus a handler name on each element.

{
  "scriptsSource": "private void q_BeforePrint(object sender, System.ComponentModel.CancelEventArgs e) {\n    var qty = Convert.ToDecimal(GetCurrentColumnValue(\"Qty\"));\n    if (qty < 0) e.Cancel = true;\n    else ((ZRLabel)sender).ForeColor = \"red\";\n}\n",
  "bands": [{
    "id": "d", "kind": "Detail", "controls": [
      { "id": "q", "type": "ZRLabel", "binding": "Qty", "scripts": { "beforePrint": "q_BeforePrint" } }
    ]
  }]
}

The engine wraps the source in a temporary class (one field per named control), compiles it through RuntimeCompiler, and calls every event in the catalog during the HTML render whenever a method name is assigned to the element. The order on a control: EvaluateBinding (e.Value) → the summary (SummaryInit / Reset / RowChanged / GetResult / Calculated; e.Handled replaces the total) → TextChanged → BeforePrint → SizeChanged / LocationChanged / ParentChanged → Draw / HtmlItemCreated → the Preview mouse (PreviewClick and related events fire while Preview is being drawn, not from a live hit test) → emit → AfterPrint → PrintOnPage (the sheet index and count on a page header/footer; on Detail rows, after the chrome has been laid out). On the report, before printing: ParametersRequestBeforeShow / ValueChanged / Submit → BeforePrint → DataSourceDemanded; on every row DataSourceRowChanged; after the body FillEmptySpace; on every section BandHeightChangedandPrintProgress. In a script ZRLabel/ZRCheckBox/ZRTableCell/ … are script-facing types backed byZrScriptControl (Text, Visible, ForeColor, BackColor, Bold). The composer emits only ZR control names. Colours are CSS (red, #c00), not System.Drawing`.

The lightning tab and the Scripts tab use one catalog: picking an event writes private void Name_Event(...) into scriptsSource and stores the name on scripts.beforePrint and the like. Check source compiles the wrapper (POST …/check-scripts). That same pass is part of compiling the model (POST /api/metadata/models/compile and …/models/{id}/compile): a broken scriptsSource fails the model exactly as a broken event handler does.

A compilation error does not bring down the preview: the form is drawn without the scripts, with a yellow strip on top carrying the error text.

Configure sections from code#

Report scripts can change the live band collection in the report-level BeforePrint, before detail rows are collected and pages are planned. Use FindBand, FindControl, FindTableRow, and FindTableCell with an element id or name. Set the returned type's properties directly: bands have Visible, Style, Back, Fore, FontFamily, FontSizePt, Borders, HeightMm, and WidthMm; controls have Text, Visible, WMm, HMm, and formatting properties; table rows have HeightMm; table cells have Text, Back, Borders, and alignment properties. A null band WidthMm keeps the default full printable width; a positive value narrows the section. Style accepts a named style id or name from the layout. MoveBand changes the order within the same renderer band rank and group level; it cannot move a Detail before a ReportHeader or a ReportFooter before Detail.

using ZuloOne.Printing;

private void Report_BeforePrint(object sender, System.EventArgs e)
{
  var appendix = FindBand("Appendix");
  if (appendix != null)
  {
    appendix.Visible = Header.TryGetValue("IncludeAppendix", out var include)
      && include is true;
    appendix.Style = "Muted";
    appendix.StylePriority = new ZrStylePriority
    {
      UseBackColor = false,
      UseForeColor = false,
      UseFont = false,
      UseBorders = false,
    };
    appendix.WidthMm = 160;
  }

  var total = FindControl("TotalLabel");
  if (total != null)
  {
    var showNet = Header.TryGetValue("ShowNet", out var raw) && raw is true;
    total.Text = showNet ? "Net total" : "Total";
    total.WMm = 48;
  }

  var netCell = FindTableCell("NetCell");
  if (netCell != null) netCell.Back = "#e8f4ff";

  MoveBand("Totals", 0);
}

The script sees Bands in its current order. The lookup methods match ids and names case-insensitively. Use MoveBand(idOrName, index) for a stable reorder; the index is zero-based. FindControl also searches controls nested inside panels. The renderer still enforces its structural phases (page/report headers, groups, details, footers), so moving DetailReport sections relative to one another is supported, but moving one across those phases is not.

The same report-level script can build a printout dynamically. AddBand accepts any ZrBand; AddControl accepts any renderer-supported ZrControl and a parent band or ZRPanel; AddTableRow / AddTableCell add table content. Missing ids are generated. The matching RemoveBand, RemoveControl, RemoveTableRow, and RemoveTableCell methods remove by id or name. Removing a band also removes its sub-bands and group bindings. These changes affect only the current render; they do not save back to LayoutJson.

var appendix = AddBand(new ZrBand
{
  Kind = ZrBandKind.DetailReport,
  DataMember = "WarrantyRows",
  HeightMm = 10,
});
if (appendix != null)
{
  AddControl(appendix.Id, new ZrControl
  {
    Type = "ZRLabel",
    XMm = 0,
    YMm = 0,
    WMm = 180,
    HMm = 8,
    Binding = "Text",
  });
}

RemoveControl("ConfidentialLabel");
RemoveTableCell("InternalNoteCell");
RemoveBand("InternalAppendix");
{
  "header": { "Number": "SI-1", "Customer": "<guid>" },
  "parts": { "Lines": [ { "Item": "<guid>", "Qty": 2 } ] },
  "display": { "Customer": "ACME GmbH", "Lines.0.Item": "Nail" }
}

display is built by the server (IReferenceDisplay) on the Html and Xlsx auto dataset only. That path checks display → the Detail row → the header → the text literal. ZuloReport does not use it: bindings resolve catalog cells and table columns written by PrintAsync. The format is {0:n2} (invariant).

For ZuloReport the Field List is the layout catalog. These endpoints supply the document or dictionary schema when that catalog is empty, and they still feed Html: GET /api/metadata/print-forms/{formId}/schema (GetDataTemplate) or GET /api/metadata/print-forms/schema/{documentTypeId} (the type) / GET /api/metadata/print-forms/schema/dictionary/{dictionaryId} (the dictionary). The list of a dictionary's forms: GET /api/metadata/print-forms?dictionaryMetaId=.

Layout JSON#

The units are millimetres, snap is 1 mm. Paper: A3, A4, A5, A6, B5, Letter, Legal, Tabloid, Executive, Statement, receipt tapes 44 / 57 / 58 / 80 / 112 mm (the roll width; height is the length of one tape page, and margins shrink to 3 mm) and Custom. A receipt has the roll flag enabled; changing the tape height keeps the receipt type. An unknown type is an empty rectangle (forward compatibility). The validator rejects only a broken schema (schemaVersion, no bands, coordinates < 0).

The bands (top to bottom): TopMargin, ReportHeader, PageHeader, GroupHeaderN…GroupHeader1, (VerticalHeader / VerticalDetail / VerticalTotal or Detail), DetailReport, GroupFooter1…GroupFooterN, PageFooter, ReportFooter, BottomMargin. Any section other than the sheet margins may carry a SubBand (parentBandId) — it prints immediately after its parent (on Detail, once per row). A detail stack is mandatory: either Detail or VerticalDetail (the two never coexist — adding a Vertical band removes Detail, and vice versa). TopMargin / BottomMargin are unique sections, live in the Band menu and exist on a new report from the start; their height is margins.top / margins.bottom (drag the band to write the margin; drag the margin to write the band height). Left and right are still margins.left / right (grey zones, not bands). In print TopMargin goes first in .zr-head and BottomMargin last in .zr-foot (on every sheet). A Body section with table set, and no child ZRTable that has its own table, is a row template: the engine emits it once per row of that catalog table. An unbound Body prints once; each ZRTable inside it grows from its own catalog table (Header once, Data once per row, Footer once). Chrome never repeats from a table. An old dataMember is read as that catalog table so an existing layout still opens. In print a Vertical band becomes columns inside .zr-vert (the section's heightMm is the column width).

Grouping is the groups[] array in LayoutJson (the Grouping and Sorting panel):

{ "id": "g0", "field": "Store", "sort": "Asc", "grouped": true, "headerBandId": "gh", "footerBandId": "gf" }

grouped: false sorts the Detail rows only, with no bands (a GroupHeader / GroupFooter left behind is not printed). grouped: true creates a GroupHeader / GroupFooter pair. groupLevel: 0 is the level closest to Detail (GroupHeader1). The renderer sorts the rows and repeats the header/footer whenever the key changes. The rows are the catalog table of the row-template Detail (a Detail with table and no bound child grid). Its group headers and footers bind to that table's columns — the first row of the group for a header, the last for a footer — and print only around those rows, never once on their own. Sorting is stable, so a group field that is a sort key (0001, 0002…) keeps the script's order. Example: a price list with Section › Block › Title groups over one Lines table prints each section, block and title heading once, then its lines. A band's printed height is the designer's heightMm; CanGrow / CanShrink on text expand or shrink the section to fit its content; without them, overflowing controls are clipped (overflow:hidden). Keep together is on by default (KeepTogether = true, and may be left out of the JSON): a band that does not fit as a whole moves to the next sheet. Clear the checkbox and the section is cut instead (clipTop plus a top shift), with the tail on the next sheet. A band taller than the page is cut even with Keep together on. PrintOnPage and Page-range summaries see the real sheet number, not 1/1. TopMargin / ReportHeader / PageHeader always go in .zr-head (the sheet header) and PageFooter / ReportFooter / BottomMargin in .zr-foot; the client moves them there when Core left the section in the body.

Controls that are actually drawn: ZRLabel (ZRLabel in old layouts), ZRCheckBox, ZRRichText (simple markup such as <b> survives, script/style/iframe are stripped), ZRPictureBox (smart tag — Image Source + a binding, Image hyperlink + a second binding, Sizing, Image alignment), ZRTable (every row of rows[], a cell's width = weight / Σ weights; a cell is clickable — the "Table Cell Tasks" smart tag: Text, Data binding, Format string, the rules, Can grow / Can shrink, Multiline, Word wrap; back = #rrggbb fill, borders = sides LRTB — a 0.2 mm outline, and an empty value draws no outline), ZRPanel (children in local coordinates), ZRPdfContent (a PDF badge; smart tag — Source / Source URL + bindings, Generate Own Pages, Fit Size to Content; a safe URL through <object>), ZRPdfSignature (a squiggle + Distinguished name / Reason / Location / Date, the Signature Options checkboxes), ZRCrossBandBox / ZRCrossBandLine (the cross-band box and the cross-band line: an overlay in .zr-body on every sheet the span crosses — the first sheet gets the top and the sides, the middle ones the sides, the last one the sides and the bottom; VerticalAnchor; they do not count towards the host band's height and are not drawn inside the section), ZRCrossTab (the cross-tab: the control's smart tag — the catalog table, the Data member, the Filter String, the Parameters; the cell's smart tag — the Field Name, the Summary Type, the Format string, auto width/height, visibility; a 2×2 grid on the canvas; in print one row/column/measure level plus the grand totals), ZRCharacterComb, ZRLine, ZRShape (smart tag — Stretch, Fill color, Line width, Rotation angle, the shape catalog — Rectangle / Ellipse / arrows / Triangle / Octagon / stars / lines), ZRBarCode (SVG), ZRChart, ZRSparkline (a line over chart.valueField), ZRGauge, ZRSubreport (a nested catalog table through sourceBinding / binding / chart.dataMember), ZRTableOfContents (the report bookmark plus group headers with sheet numbers), ZRPageInfo (the number / the total / the number out of the total — against the real .zr-page elements), ZRPageBreak (a new page after the band).

An embedded picture (data:image/…;base64, over 48 KB of text) is printed at the pixels its box can hold: Core re-encodes it to cover the box at 200 dpi (SkiaSharp, cubic resampling) — JPEG 85 when the picture is opaque, PNG when it has transparency — and keeps the original when the result is not smaller. A camera photo of 1600 × 1200 px in a 30 × 20 mm box prints as 237 × 178 px. Sizing Clip shows a picture at its own pixel size, so a clipped picture is never scaled. URLs (https://…, /…) go to the browser as they are.

The designer#

The form's card has three tabs: Main (the name, the engine, the bindings), Design (the ZR canvas + Preview) and Report scripts (scriptsSource, the element events in the same code editor as every other script: Check source / Members / IntelliSense). The Data script button in the card header opens that script in its own workspace tab — the same place a document's Events button opens. For ZuloReport the script is ZuloReportForm.PrintAsync (catalog writes). Html still opens PrintFormBase.GetDataTemplate / GetDataAsync. When there is no script yet, the button creates one first (for a dictionary — IDataService.GetByIdAsync, RecordId). The Data script button on a print-form row (a subtype's or a dictionary's Print forms list) opens the same editor tab. The lightning bolt in the property window opens Report scripts. The lists: /printforms, the block on a subtype's card and the Print forms tab on a dictionary. With no document type, Preview takes the dictionary records from the first Dictionary binding.

  • On the left the Tool Box — a title, the "Standard Controls" group, an icon plus a caption. A click picks the tool, a click on the canvas drops the control and returns to Pointer. Dragging off the tool box does the same. Collapsed, it is a narrow strip of icons. Sparkline / Subreport / TOC are enabled in the tool box and print with their own rendering. The children of an ZRPanel are separate boxes on the canvas: they can be dragged, selected and laid out outside the panel. What prints: Label / Rich Text, Check Box, Character Comb, Picture Box, Table (every row of the layout, seeded with an A/B/C header plus an empty row), a Panel with its children, PDF Content / PDF Signature, Cross-band Box / Cross-band Line, Cross-tab, Line, Shape, Bar Code, Chart / Sparkline / Gauge, Subreport, Table of Contents, Page Info, Page Break. On the canvas and in Preview a bar code is one and the same SVG (GET /api/metadata/print-forms/barcode and ZrBarCodeSvg): the linear ones (Code39/93/128, EAN/UPC, ITF, PostNet…), the matrix ones (QR / GS1 / EPC / Micro QR, Data Matrix, Aztec) with the ISO quiet zone (4 modules for QR) and meet, PDF417 stacked, Intelligent Mail — 65 four-state bars. GS1 QR puts FNC1 in the first position, not the GS byte.
  • The canvas is a physical sheet (paper + margins). The grey on the left and the right is only the margin from the edges, not a crop: Preview and window.print() render the whole sheet (@page margin 0, width = paper), with the controls shifted by margins.left exactly as on the canvas. The grey zones, the dashes, the triangles on the ruler and the handles on the inner edge of the margin can all be dragged; they write margins (top and bottom also keep the height of TopMargin / BottomMargin in step). On the canvas the bands sit flush, with no empty gap between Detail and the footer. Preview and print are one or more paper sheets (210×297 for A4): a body that does not fit, a pageBreak Before/After and an ZRPageBreak all open the next .zr-page. @page margin 0 and controls shifted by margins.left; .zr-page also carries a padding-right from margins.right. The band stretches across that padding to the paper edge (background-clip: content-box keeps the band fill on the content box), so a control's right border on the margin is not clipped; the header (TopMargin / ReportHeader on the first sheet only / PageHeader on every one) goes in .zr-head, the footer (PageFooter on every one / ReportFooter on the last one only / BottomMargin) in .zr-foot (margin-top:auto) — pinned to the bottom of the sheet. A GroupHeader with repeatEveryPage repeats at the top of the group's continuation. Core HTML (a content width + @page margins) is rearranged into this geometry on the client. The gear and the Report { Paper: A4 } tag both open Page setup…. The ruler along the top is centimetres across the full width of the sheet, the one down the left side follows the bands: dragging along the scale selects the controls aligned with that range, and a click drops a guide; the grip (☰) on a section's header moves the header and resizes the section from the top. A section strip is a grey full-width header (a triangle + an icon + the name + where it prints, e.g. [Bottom of page · Last], [in the flow, once], [once per row · Lines] / (Level N)). Collapsing hides only the band body; the strip stays. A GroupHeader's icon is red, a GroupFooter's green. There is no section-editor modal: that dialog was a second copy of Explorer, and the two copies drifted. Add and delete are Explorer and the canvas band menu (page header, page footer, another body). Placement, pages, and the section's catalog table are the right pane when the section is selected. Right-clicking the sheet gives the same add tree. A group and a DetailReport can be added again. You can collapse the other bands. Over the printable area there is a square grid of light lines (not dots), eight blue handles, and a smart tag on a ZRLabel. Dragging or arrow-keying across a band boundary moves the control into the other band. Right-click a control: z-order, Align to table, Add table row / Remove last table row, Text, the clipboard, Delete, Properties. Dropping a control onto an ZRPanel puts it into controls[] (local x/y).
  • On the right: the property window — a name|value table. On the report's Appearance tab: Watermark (…), a 3×3 Text Alignment, Borders, Paddings (a summary + L/R/T/B), the rules and styles, the colours in a strip, the border width, the font on one line (family + size), then B I U S, Right to left. Behavior on the report: Request parameters, Report print settings (Show print warning margins / Show print status window), Vertical splitting and Horizontal splitting (Exact / Smart), Visibility, Measure units (Millimeters), Export options, Extensions, Show page margins in preview, the scripts (… opens the lightning bolt) and the Script language. Data: Image resources / Data adapter / Calculated fields / catalog cells and tables / XML data path / Tag / Filter / bind-to / Parameters ((Collection) + …). Page: Landscape page orientation, Page height (mm), Print margins (a Top; Left; Right; Bottom summary + ← → ↑ ↓), Paper name, Use default printer settings, Printer, Roll paper, Paper kind, Page width (mm). The sizes are in millimetres. Layout: (Name), Localization, Snap grid step count, Developer options, Show grid, the snapping, Draw watermark, the schema, the Language, the Bookmark, Sections. Favorites is a flat list: Watermark, Style sheet, the catalog, Filter, bind-to, Report print settings, Measure units (Millimeters). A category header is not repeated within one tab. Search still ignores the tab. The Appearance / Behavior / Data / Page / Layout / Favorites tabs plus the lightning bolt all narrow the list down. The property rows are ZrPropObject plus a per-type descendant with its own spec. Section placement, pages, and the catalog table are rows in this grid; there is no section modal. Paper and margins are the Page rows, not a second dialog. The Properties / Explorer / Fields tabs are along the bottom. Ctrl+click for several controls: edits in the property window and on the font/colour toolbar apply to all of the selected ones (name stays with the last one). On the canvas the same holds for the mouse: dragging a resize grip of any selected control resizes every selected control from the same edge by the same distance (pull the left edge of one, the left edges of all move, each right edge stays), and dragging one of them moves the whole selection. A plain click on a selected control, without dragging, leaves only that control selected. Typing in the smart-tag card (Text, Data binding, or a dropdown) does not move the control and does not close the card: a click on a table cell selects that cell; drag the table from its edge. Delete / Backspace on a selected cell clears the cell and leaves the grid. Escape closes the card. Arrow keys while a field or list is focused stay with the field. Ctrl+B bolds the selected cell when a cell is active.
  • A two-row toolbar: save / clipboard / undo, font and colour (a dropdown of Custom / Web / System plus "More colors…"), text alignment; the grid, object alignment, H/V spacing, same size, z-order, zoom. Ctrl+click for several controls; Ctrl+C/X/V. The arrow keys nudge the selected controls by 1 mm, Shift+arrow by 5 mm (inside property fields the arrows still belong to the input).
  • The Grouping and Sorting panel: the field, Asc/Desc, Show header / Show footer, Move up / Move down; right-click a row to add a subsection or delete it. Its height is dragged from the top edge (a splitter), and the canvas shrinks.
  • Right-click a group band (the gutter, the canvas, the explorer): Add subsection, move it, Delete group, Properties
  • The Field List is the catalog: cells, then tables and their columns, then calculated fields and parameters. Drag a cell to bind a label; drag a table onto a Body to drop a ZRTable (Header + Data); drag a column onto a Data cell. It is not the columns of a SlimTable. Calculated fields stay a formula layer on those names (right-click: Add / Edit / Delete all). Double-clicking a calculated field opens the Expression Editor.
  • The Report scripts tab: one C# scriptsSource per form. The editor is CodeEditorWorkbench (checking through POST …/check-scripts, completion through POST …/complete-scripts via ZrScriptComposer). Across the top: the object (Report Report, tableRow1 Table row, tableCell1 Table Cell, …) and the event Caption (EventName), its own set for every type: the report gets PrintProgress / DataSourceDemanded / ParametersRequest* / BeforePrint…, a running header/footer and the sheet margins (PageHeader / PageFooter / TopMargin / BottomMargin) get BeforePrint and AfterPrint only, a table and a table row get PreviewMouseMove / PreviewClick / EvaluateBinding / BeforePrint… (no summary, no TextChanged, no HtmlItemCreated), a table cell gets SummaryRowChanged / SummaryCalculated / TextChanged / EvaluateBinding / HtmlItemCreated… (no SummaryInit), a label gets EvaluateBinding / PreviewMouseUp / the summaries… Picking an event writes private void Name_Event(...) at once and stores the method name on scripts.beforePrint and similar properties. Check source compiles the wrapper (POST …/check-scripts). VB and JScript are not compiled. In the code: GetCurrentColumnValue("Qty"), ((ZRLabel)sender).ForeColor = "red", e.Cancel = true, e.Value inside EvaluateBinding. The language in the property window is C# only.
  • The report Explorer: the sections, the panel→children and table→row→cell nesting, and search. Add and delete sections here. There is no separate section editor; the data model is the catalog.
  • The report's smart tag (the gear on the sheet): the title is the report's name (displayName / name, otherwise "Report"), not "Tasks". The actions: a row Filter ([Qty] > 0), Measure units (Millimeters), Watermark, Page color. It does not edit sections. A Body section's gear sets the catalog table. On a section the title is GroupHeader1 / Detail / the band's name. Clicking the Report { Paper: A4 } tag selects the report (the sheet's property window).
  • The smart tag (the gear) on the field controls: ZRLabel / Check Box / Rich Text / Character Comb — Text, Data binding, Format string, Summary; ZRBarCode — the Barcode type from the catalog (1D: Code11/39/93/128, EAN/UPC, ITF-14, PostNet, GS1 DataBar…; 2D: QR / GS1 QR / EPC QR / Micro QR, Aztec, DataMatrix, PDF417), Module, Automatic module, Orientation, Text, Data binding, Format string, Show text; ZRGauge — the View type / View style / View theme, the Actual value and the Target value, the Minimum / Maximum plus a binding for each; ZRChart — the catalog table, the Data member (a part), the Value field / Argument field, the View (Line, Bar, Pie…); ZRSparkline — the same, without the argument field and the view; ZRSubreport — the Data member (a catalog table); ZRTableOfContents — a hint about where the entries come from; ZRCrossTab — the control's gear: the catalog table, the Data member, the Filter String, the Parameters; clicking a cell (Row Area / Column Area / Data Area) switches the smart tag to Cross Tab Cell: the Field Name, the Summary Type, the Format string, Column Auto Width Mode / Row Auto Height Mode, Column Visible / Row Visible; Picture Box — URL / binding / Sizing; ZRPdfContent — Source / Source URL + bindings, Generate Own Pages, Fit Size to Content; ZRPdfSignature — Image Display Mode, Display Document Signature, Show Captions / Show Certificate Name / Show Distinguished Name / Show Location / Show Signature Date / Show Signature Reason; ZRCrossBandBox / ZRCrossBandLine — Vertical binding (None / Top / Bottom / Top and bottom); Page Info — the number type. An orange dot = there is a binding
  • The property window: five tabs (a brush / a gear / a cylinder / a spanner / a star) — Appearance / Behavior / Data / Layout / Favorites. The lightning bolt on the right opens events. On the report there is a fourth tab, Page (a sheet icon) — Landscape page orientation, Page height (mm), Print margins (expanded ← → ↑ ↓), Paper name, Use default printer settings, Printer, Roll paper, Paper kind, Page width (mm). The Layout tab (the spanner) on the report: (Name), Localization, Snap grid step count, Developer options (the units), Display name, Show grid, the snap size and Snap mode, Draw watermark, the catalog, the Language, the Bookmark, Sections. Favorites (the star) on the report: Watermark, Style sheet, the catalog, Filter, bind-to, Report print settings, Measure units. Appearance — Watermark, the alignment, Borders, Paddings, the collection of rules and styles, the Page color / Text color / Background color, the Font, Right to left. Behavior — Request parameters, printing and export, the splitting, Visibility, Measure units (Millimeters), Extensions, the Preview page margins, the scripts and the C# language. On Data: Image resources, the Data adapter (None), the Calculated fields, catalog cells and tables, the XML data path, Tag, Filter (… is the Expression Editor), Data member and Parameters. The shared fields (appearance, visibility, size) exist on the report, on a band and on a control; the type-specific ones (the bar code, the Gauge, the Chart, groupField, the paper) appear on their own tab. A band (ReportHeader, GroupHeader, …) has the same Appearance a field does: a 3×3 alignment, Borders, Paddings, the styles and rules, the dash, the colours, the B/I/U/S font, the height. On a label the Appearance tab is the alignment, Borders, Text Trimming, Paddings, the rules, Style Priority, the styles, the dash, the colours (the background is Transparent by default), the width, the Font, the Position, the Size, Right to left. Appearance — a 3×3 alignment (with no duplicate L/C/R/Justify under the grid), Borders (All borders / No borders / individual sides), Paddings All / L / R / T / B, the Border dash style, the width slider, the B/I/U/S font, the colours. Formatting Rules are a collection on the form (formattingRules[], the Condition is that same [Qty] < 0 expression); on a control and on a band it is formattingRuleIds. An empty condition is always true: the rule paints on the canvas and in print immediately; a non-empty one only when the expression is true for the row. Styles are named presets plus odd/even; Style Priority decides what is taken from the object and what from the style. The f button on Text / Visible / ForeColor / BackColor opens the Expression Editor.
  • The status bar: the object + the paper (A4 / Letter) + the zoom. The zoom scales Preview too, on screen only — printing from Preview stays 1:1.
  • Preview posts the current JSON to POST …/preview/{documentId}
  • Check in the status bar is the form's "compile" (see below). The badge next to it shows errors/warnings of the last check; click it to reopen the list.

Checking a form#

The renderer never fails on a bad binding: an unknown field prints an empty cell (a [Name] in Preview), a formula with a typo evaluates to empty, an event that names a missing method is skipped. Check finds all of these before paper does. Each finding is a row in the Form check dialog; a click selects the control, the table cell or the band it is about.

The canvas shows the same findings in place. The designer re-checks the canvas about a second after every edit (without the trial print) and paints:

  • a control or a table cell with an error in a solid red frame, with a warning in a dashed amber one, plus a small ! in its top-right corner;
  • a band with a band-level finding (DetailWithoutDataMember, an orphan band) with a ! in its title bar.

Hover the frame or the ! to read the findings. Fixing the cause clears the mark on the next re-check. Report-level findings (page geometry, scripts, the data script) have no element to mark: they are in the status-bar badge and the dialog only.

POST /api/metadata/print-forms/{id}/check with { "layoutJson": "…", "recordId": "…" } (both optional: no layoutJson checks the saved layout; with recordId the check also prints that record once). The answer is always 200:

{ "ok": false, "errors": 1, "warnings": 0, "dataSource": "DataScript", "pages": 1,
  "diagnostics": [ { "severity": "Error", "code": "UnknownField",
    "message": "Label 'c-item': [Itme] is not in the header or the rows of 'Report'. Did you mean [Item]?",
    "bandId": "detail", "controlId": "c-item", "cellId": null, "property": "binding" } ] }

Zr fields are checked against the layout catalog (cells / tables). When the catalog is empty, the check uses the document type or the dictionary, plus the system columns every row carries (MetaId, ID, DocumentDate, CreatedDateTime, …). It does not call GetDataTemplate on a ZuloReportForm. Html still checks GetDataTemplate. The designer sends the unsaved canvas and the newest document of the form's type for the trial run.

Code Severity What it means
UnknownField Error A binding, a table cell, a formula, a summary, a group, a chart or a cross-tab names a field the data does not have. The message suggests the closest name.
DetailWithoutDataMember Error Detail binds row fields but has no dataMember: it prints once, with empty cells. The message names the part to set.
RowFieldOutsideDetail Error A header or footer band binds a row field without a summary: it prints empty.
UnknownDataMember Error dataMember of the report, a band, a chart, a cross-tab or a subreport is not a part of the data.
ExpressionSyntax Error A formula does not parse, or calls an unknown function (Iff).
MissingScriptMethod Error An event names a method that scriptsSource does not define; it is skipped silently at run time.
ReportScripts Error scriptsSource does not compile.
DataScript / DataScriptRun Error The Zr script does not compile, it is not a ZuloReportForm, or (trial run) PrintAsync throws. Print does not fall back to the raw record.
RenderFailed Error The trial print threw.
MissingBand Error A group points to a header/footer band that does not exist.
DuplicateId Error Two controls share an id.
NoRoomForRows Error Margins + page header + page footer leave less than 8 mm of a page.
OutsidePrintableArea Warning A control runs past the right margin or starts at a negative position.
OutsideBand Warning A control in a fixed-height band (margins, page and report header/footer) is taller than the band; the overflow is clipped.
BandTallerThanPage Warning Detail is taller than the body of a page; every row is cut across pages.
NoRoomOnFirstPage Warning Report header and footer leave no rows on page 1.
MissingFormattingRule / MissingStyle Warning A rule id or a style name that the form does not define.
OrphanGroupBand / OrphanSubBand Warning A band nothing prints.
ShadowedField Warning A calculated field has the name of a data field and overwrites it.
EmptyGroupField / EmptySummary / EmptyName Warning A row with nothing to act on.

GET /api/metadata/print-forms/check-all checks every saved ZuloReport form on the stand (no trial run) and returns one result per form.

Multi-page print#

Pages are planned on the server; the browser and the PDF service lay out exactly the same sheets (.zr-page, one per sheet, fixed height).

  • TopMargin, PageHeader, PageFooter, BottomMargin print on every sheet. ReportHeader prints on the first sheet only, ReportFooter on the last.
  • The body of a sheet is the paper height minus the per-page bands; the first sheet also loses the report header. The report footer is not reserved on every sheet: when the last rows leave no room for it, it gets a sheet of its own.
  • A Detail row (keepTogether: true, the default) never splits: a row that does not fit starts the next sheet. A band taller than a whole sheet is cut.
  • GroupHeader with repeatEveryPage is repeated at the top of each sheet its group continues on — including the sheet that holds only the group footer. A new group starting at a sheet break does not repeat the previous group's header.
  • Group sorting keeps the data order inside a group.
  • pageBreak: "BeforeBand" / "AfterBand" and an ZRPageBreak control force a new sheet.

Text that does not fit#

A label grows to fit its text (canGrow is on unless the layout sets "canGrow": false, as in DevExpress). The page planner estimates the height of the wrapped text — Segoe UI at the control's point size, wrapped at spaces, plus padding and borders — and grows the band by that much. Controls that share a top and a height form a row (a table drawn with labels): the row grows as one, so a wrapping cell does not leave its neighbours' borders short. A control with canGrow: false keeps its height and clips the text.

The canvas draws text exactly as print does: the font size is in points (9 pt = 12 px at 96 DPI, scaled by the zoom), line-height: normal, and a label without its own padding gets 0 0.5 mm on both canvases.

A gap between a table's header and its rows is empty space inside the bands, not a margin: the column headers end above the PageHeader band's bottom edge, or the Detail cells start below Detail's top edge. Fit band to controls (the band's context menu, or its smart tag) moves the band's controls up to its top edge and ends the band at their bottom. Apply it to both bands and the header touches the first row. New forms are seeded that way.

Expressions (formulas)#

A language of its own, with sugar — [Qty] * [Price], Iif([Qty]<0,'red','black'), Sum([Amount]). Not the NCalc package, not eval, and not compiling formulas into C#. The data script and the element scripts stay C#.

Calculated fields are LayoutJson.calculatedFields[]. On a control they are expressions.text | visible | fore | back. The aggregates (Sum/Avg/Count/Min/Max) are computed over the current group (GroupFooter) or over the whole part (ReportFooter / a calculated field).

A footer summary is a summary object on a ZRLabel in a GroupFooter / ReportFooter / PageFooter: the Summary function, the Bound field, the Calculation range (None/Page/Group/Report), the Format string and ignoreNull. The canvas shows [Sum(Qty)]; Preview does the arithmetic. Page counts the rows that landed on that sheet. RunningSum is a cumulative total down the part's rows; Percent is the slice's share of all the rows.

The order at render time: catalog values (ZuloReport) or the Html table → calculated fields → the named style (odd/even) → binding → expressions.text → summary → the C# EvaluateBinding → format → visible/fore/back → the formatting rules → BeforePrint. An error in a formula yields null (the form does not fail). The canvas evaluates no formulas — only Preview does.

The editor catalog is the engine itself: Iif, IsNull, IsNullOrEmpty, Abs/Ceiling/Floor/Round, Concat/Contains/StartsWith/EndsWith/Len/Lower/Upper/Trim, AddDays, the aggregates. Min/Max with two arguments is a scalar, with a single field it is an aggregate.

{
  "calculatedFields": [
    { "id": "cf1", "name": "LineTotal", "expression": "[Qty] * [Price]", "fieldType": "Decimal" }
  ],
  "bands": [{
    "kind": "GroupFooter",
    "controls": [{
      "type": "ZRLabel",
      "text": "[Sum(LineTotal)]",
      "summary": { "function": "Sum", "field": "LineTotal", "range": "Group", "ignoreNull": true },
      "expressions": {
        "fore": "Iif([Qty] < 0, 'red', 'black')",
        "visible": "[Qty] != 0"
      }
    }]
  }]
}

A gauge (ZRGauge):

{
  "type": "ZRGauge",
  "wMm": 40,
  "hMm": 22,
  "gauge": {
    "viewType": "Circular",
    "viewStyle": "Half",
    "viewTheme": "FlatLight",
    "valueBinding": "Qty",
    "min": "0",
    "max": "100"
  }
}

A chart (ZRChart):

{
  "type": "ZRChart",
  "wMm": 60,
  "hMm": 30,
  "chart": {
    "view": "Line",
    "dataMember": "Lines",
    "valueField": "Qty",
    "argumentField": "Name"
  }
}

What the engine does not do#

Foreign binary layouts, QuestPDF, print agents / a printer queue, and copies in the dialog. VB element scripts. Live evaluation of formulas on the canvas. Cutting a single control through the middle of a glyph is not supported: Keep together keeps the band whole; if that fails, the renderer clips at the sheet boundary (overflow:hidden) rather than reflowing the text. A Vertical band is a stack on the canvas; the columns exist in Preview and in print only. A Cross-tab is one row level and one column level. PDF Content does not rasterise the pages of a PDF — the browser draws an <object> (Generate Own Pages opens a new sheet). A Subreport prints a nested catalog table (not another print form). The Table of Contents is the report bookmark plus group headers with sheet numbers. A Sparkline is a line over chart.valueField.

The assistant (not PDF, not the Cursor MCP)#

The built-in assistant edits a ZuloReport layout through the same IAssistantTool mechanism as query_records. Not a separate MCP server, and not PDF generation.

Tool What it does Right
list_print_forms The forms the user may Read Read on the bound document / dictionary
get_print_form The card + layoutJson Read
set_print_form_layout Replaces LayoutJson after ZrLayoutParser.Parse Read and Update. Engine=Zr only
preview_print_form HTML: a url + a truncated excerpt. No file, no PDF Read

Invalid JSON (no bands, schemaVersion < 1) is refused, and the database row is left alone. Html/Xlsx cannot go through set_print_form_layout.

Same topic for other readers: User

Where next#

Was this page helpful?