Table of Contents

Dynamic report construction

This example demonstrates how to construct a complete report layout—including page setup, static text objects, a report container with a data-bound table—entirely through the DOM API. No pre-designed .lst or .json project file is required.


Overview

The workflow consists of two phases:

  1. Build – Create a project in memory using the DOM, define objects and data bindings, then save.
  2. Export – Load the in-memory project together with a data source and export to the desired format.

Both phases can run without file system access, making this approach suitable for serverless, containerized, or browser-based (WASM) deployments.


Full example

The following service class builds a customer list report and exports it to PDF. All operations happen in-memory.

using combit.Reporting;
using combit.Reporting.DataProviders;
using combit.Reporting.Dom;

public class DynamicReportService
{
    // A4 dimensions in SCM (1/1000 mm).
    private const int PageWidth = 210_000;
    private const int PageHeight = 297_000;

    // Page margins.
    private const int MarginLeft = 20_000;
    private const int MarginTop = 15_000;
    private const int MarginRight = 20_000;
    private const int MarginBottom = 15_000;

    private const int ContentWidth = PageWidth - MarginLeft - MarginRight;
    private const int ContentHeight = PageHeight - MarginTop - MarginBottom;

    // Column widths (must sum to ContentWidth).
    private const int ColId = 15_000;
    private const int ColName = 60_000;
    private const int ColCity = 55_000;
    private const int ColRevenue = 40_000;

    // Title layout.
    private const int TitleHeight = 12_000;
    private const int TitleGap = 2_000;

    /// <summary>
    /// Builds a report layout via the DOM and exports it to PDF.
    /// </summary>
    public byte[] GenerateCustomerReport(IReadOnlyList<Customer> customers)
    {
        ArgumentNullException.ThrowIfNull(customers);

        using MemoryStream projectStream = BuildProject();
        using MemoryStream pdfStream = ExportToPdf(customers, projectStream);

        return pdfStream.ToArray();
    }

    // ──────────────────────────────────────────────────────────────────────
    // Phase 1: Build the project via the DOM
    // ──────────────────────────────────────────────────────────────────────

    private MemoryStream BuildProject()
    {
        using var ll = new ListLabel();
        var projectStream = new MemoryStream();

        using ProjectList project = ll.OpenProject(projectStream, LlProject.List)
            ?? throw new InvalidOperationException("Failed to create project.");

        AddTitle(project);
        AddReportContainer(project);

        project.Save();

        // Return an independent copy so the caller owns the stream lifetime.
        return new MemoryStream(projectStream.ToArray());
    }

    private void AddTitle(ProjectBase project)
    {
        ObjectText title = project.Objects.AddNewText();
        title.Position.Set(MarginLeft, MarginTop, ContentWidth, TitleHeight);

        Paragraph paragraph = title.Paragraphs.AddNew();
        // String literals in LLCP formulas are enclosed in double quotes.
        paragraph.Contents = "\"Customer Revenue Report\"";
    }

    private void AddReportContainer(ProjectBase project)
    {
        ObjectReportContainer container = project.Objects.AddNewReportContainer();

        int containerTop = MarginTop + TitleHeight + TitleGap;
        int containerHeight = ContentHeight - TitleHeight - TitleGap;
        container.Position.Set(MarginLeft, containerTop, ContentWidth, containerHeight);

        SubItemTable table = container.SubItems.AddNewTable();
        // TableId must match the name exposed by the data provider.
        table.TableId = nameof(Customer);

        AddHeaderLine(table);
        AddDataLine(table);
    }

    private void AddHeaderLine(SubItemTable table)
    {
        var headerLine = new TableLineHeader(table.Lines.Header);

        AddTextField(headerLine.Fields, ColId, "\"#\"");
        AddTextField(headerLine.Fields, ColName, "\"Name\"");
        AddTextField(headerLine.Fields, ColCity, "\"City\"");
        AddTextField(headerLine.Fields, ColRevenue, "\"Revenue\"");
    }

    private void AddDataLine(SubItemTable table)
    {
        var dataLine = new TableLineData(table.Lines.Data);

        // Field references follow the pattern "TableName.PropertyName".
        AddTextField(dataLine.Fields, ColId, "Customer.Id");
        AddTextField(dataLine.Fields, ColName, "Customer.Name");
        AddTextField(dataLine.Fields, ColCity, "Customer.City");
        AddTextField(dataLine.Fields, ColRevenue, "Customer.Revenue");
    }

    private static void AddTextField(
        CollectionTableFieldBases fields, int width, string contentsFormula)
    {
        var field = new TableFieldText(fields);
        field.Width = width.ToString();
        field.Contents = contentsFormula;
        field.Wrapping.Line = "1";
        field.Wrapping.Force = "True";
    }

    // ──────────────────────────────────────────────────────────────────────
    // Phase 2: Export the project
    // ──────────────────────────────────────────────────────────────────────

    private static MemoryStream ExportToPdf(
        IReadOnlyList<Customer> customers, MemoryStream projectStream)
    {
        var dataProvider = new ObjectDataProvider(customers);

        using var ll = new ListLabel
        {
            DataSource = dataProvider,
            LicensingInfo = string.Empty
        };

        var pdfStream = new MemoryStream();
        var exportConfig = new ExportConfiguration(
            LlExportTarget.Pdf, pdfStream, projectStream);
        exportConfig.ShowResult = false;

        ll.Export(exportConfig);

        pdfStream.Seek(0, SeekOrigin.Begin);
        return pdfStream;
    }
}

Key concepts

Coordinate system

All positions and sizes use SCM (1/1000 mm) units. A 2 cm margin equals 20,000 SCM.

Formula syntax

DOM property values that represent formulas follow LLCP formula rules:

  • String literals are enclosed in double quotes: "\"Hello\"".
  • Data field references use the pattern TableName.FieldName without quotes.

TableId and data binding

The SubItemTable.TableId must match the table name exposed by the data provider. When using ObjectDataProvider with a collection of Customer objects, the table name is the class name ("Customer").

Stream-based workflow

Both OpenProject and ExportConfiguration accept Stream instances. This enables fully in-memory operation without file system dependencies—ideal for Docker, serverless functions, or WebAssembly.


Variations

var footerLine = new TableLineFooter(table.Lines.Footer);
AddTextField(footerLine.Fields, ColId + ColName + ColCity, "\"Total\"");

var totalField = new TableFieldText(footerLine.Fields);
totalField.Width = ColRevenue.ToString();
totalField.Contents = "Sum(Customer.Revenue)";

Using a file path instead of a stream

When file system access is available, you can pass a file path to OpenProject:

using ProjectList project = ll.OpenProject(
    "report.json", LlDomFileMode.Create, LlDomAccessMode.ReadWrite, LlProject.List);

Changing the paper format

To use a different paper format, set the size of the region to the required dimensions:

// First region of the project
var region = project.Regions[0];

// US Letter: 8.5 x 11 inch = 215.9 x 279.4 mm
region.Paper.Extent.Set(new Size(215900, 279400));

Constraints and considerations

  • DOM manipulation requires understanding of report structure and SCM units. Start with small experiments.
  • Many DOM properties are typed as string because they may contain dynamic expressions. Pass "True" / "False" for booleans and numeric strings for dimensions.
  • Always call project.Save() before passing the stream to export.
  • For WASM deployments, all operations must be synchronous and stream-based because the browser sandbox has no file system access.
Tip

To inspect the generated project structure, save to a file and open it in a text editor—the format is human-readable JSON.


See also