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:
- Build – Create a project in memory using the DOM, define objects and data bindings, then save.
- 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.FieldNamewithout 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
Adding a footer line
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
stringbecause 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.