ReportingCloud

Nested Blocks

Topic

A sales report that lists every sale with the items sold in it has data on two levels, and its template needs a merge block inside a merge block. Building one takes three parts: JSON data with an array inside an array, a datasource excerpt that shows its structure to the template editor and a template with nested blocks. Afterward, you merge the report with curl or with the .NET SDK.

Blocks at one level, their names and their data are explained in Merge Blocks. The template is designed with two tools of the ReportingCloud portal, which Datasource Excerpts and Template Editor describe in full.

How Nested Blocks Work

A master-detail relationship is a one-to-many relationship: one sale has many items. In the template, the outer block, sales, is repeated for each sale, and the inner block, items, lies inside it and is repeated for each item of the current sale:

  • Block names match the arrays. The outer block is named like the array of sales, and the inner block like the array of items inside each sale.
  • Fields take the values of their own level. In the items block, qty and price are the values of the current item. The field id in the sales block is the number of the current sale, and name, outside both blocks, belongs to the report.
  • Nested objects use dot notation. The field product.name in the items block takes name from the product object of the current item.
  • Levels have no limit. Blocks can be nested in any number of levels, each inside the block of its parent array.

The following template has the outer block sales, with the field id and a field master_Id that the sample data below doesn't fill, and the inner block items in a table of its own, with the fields price, qty and product.name. The report's name is above both blocks:

Template with an items merge block nested in the sales block

Shape the Data

The data has a level for each block. Save the following as sales.json:

[
  {
    "name": "Monthly Sales Report",
    "sales": [
      {
        "id": 1,
        "items": [
          { "qty": 5, "price": 256.2, "product": { "name": "ReportingCloud" } },
          { "qty": 3, "price": 424.2, "product": { "name": "TX Text Control" } }
        ]
      },
      {
        "id": 2,
        "items": [
          { "qty": 5, "price": 256.2, "product": { "name": "ReportingCloud" } },
          { "qty": 3, "price": 424.2, "product": { "name": "TX Text Control" } }
        ]
      }
    ]
  }
]

The levels of the data and of the template:

LevelIn the dataIn the template
RecordThe object in the outer array, with nameTop-level fields: name
1The array salesMerge block sales with the field id
2The array items in each sale, with an object productMerge block items inside sales, with the fields price, qty and product.name

The array around the whole object is required: mergeData is an array of records, and the report is its only record, so the template is merged once.

Create the Datasource Excerpt

The template editor learns the names of the blocks and fields from a datasource excerpt. It uses the excerpt to fill its lists and to preview the template; it isn't the data you merge.

  1. In the portal, open My Datasource Excerpts.
  2. Enter a name for the excerpt and paste the content of sales.json into the Datasource Excerpt box. The data must be an array of objects, so keep the square brackets around the report.
  3. Save the excerpt with Create Excerpt.

The screenshot shows an excerpt of the same structure with capitalized names and one sale. The editor lists the names in lowercase either way:

New datasource excerpt with nested Sales and Items JSON

Design the Template

  1. In My Templates, create a new template with New Template..., enter a name, such as sales_report, choose a format and confirm with OK. Before the editor opens, select the excerpt in Select datasource excerpt file and confirm with Edit Template:

    Selected datasource excerpt file circled above the Edit Template button

  2. In the editor, choose sales from the Merge Block drop-down on the Reporting tab, check id in the dialog that opens and confirm with OK. The outer block is inserted as a table row.

  3. Place the input position in the row of the sales block, right-click and choose Insert and Table Row Below:

    Context menu inserting a table row below the block row

  4. Place the input position in the new row and choose items from the Merge Block drop-down.

  5. In the Insert Merge Block dialog, check price and qty, leave the other columns unchecked and confirm with OK:

    Insert Merge Block dialog with price and qty checked

    A second block, with a header row of its own, is now inside the outer block:

    Nested price and qty block inside the outer sales block

  6. Click Preview Merge Results on the Reporting tab and confirm the dialog. Each sale is followed by its items:

    Merge preview listing nested price and qty rows per sale

  7. From the Merge Field drop-down, add the other fields of the first screenshot: name above the table and product.name in a third column of the items row. Save the template.

The requests below use the name sales_report.tx. With a format other than TX, use the extension of that format.

Merge the Report

The following commands merge sales.json into the template and save the report:

jq '{mergeData: .}' sales.json > merge.json

curl -X POST "https://api.reporting.cloud/v1/document/merge?templateName=sales_report.tx&returnFormat=PDF&append=true" \
  -H "Authorization: ReportingCloud-APIKey $REPORTINGCLOUD_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @merge.json \
  -o result.json

jq -r '.[0]' result.json | base64 -d > report.pdf

With the .NET SDK, the same data comes from classes. The SDK writes their property names in camel case, so Sales, Items and Qty are sent as sales, items and qty, the names of the template:

using TXTextControl.ReportingCloud;

Report report = new Report
{
    Name = "Monthly Sales Report",
    Sales = new List<Sale>
    {
        new Sale
        {
            Id = 1,
            Items = new List<Item>
            {
                new Item { Qty = 5, Price = 256.2m, Product = new Product { Name = "ReportingCloud" } },
                new Item { Qty = 3, Price = 424.2m, Product = new Product { Name = "TX Text Control" } }
            }
        },
        new Sale
        {
            Id = 2,
            Items = new List<Item>
            {
                new Item { Qty = 5, Price = 256.2m, Product = new Product { Name = "ReportingCloud" } },
                new Item { Qty = 3, Price = 424.2m, Product = new Product { Name = "TX Text Control" } }
            }
        }
    }
};

ReportingCloud rc = new ReportingCloud(Environment.GetEnvironmentVariable("REPORTINGCLOUD_API_KEY"));

// A list with one report: the template is merged once
MergeBody mergeBody = new MergeBody { MergeData = new List<Report> { report } };

List<byte[]> documents = rc.MergeDocument(mergeBody, "sales_report.tx", ReturnFormat.PDF, true);
File.WriteAllBytes("report.pdf", documents[0]);

public class Report
{
    public string Name { get; set; } = "";
    public List<Sale> Sales { get; set; } = new List<Sale>();
}

public class Sale
{
    public int Id { get; set; }
    public List<Item> Items { get; set; } = new List<Item>();
}

public class Item
{
    public int Qty { get; set; }
    public decimal Price { get; set; }
    public Product Product { get; set; } = new Product();
}

public class Product
{
    public string Name { get; set; } = "";
}

The code works as follows:

  • Each class is one level of the report: Report holds the list of Sale objects for the sales block, and each Sale the list of Item objects for the items block. Product is the nested object of product.name.
  • The SDK sends exactly the content of sales.json as mergeData, because the report is wrapped in a list.
  • MergeDocument merges the stored template sales_report.tx and returns a list of byte arrays, here with the PDF as its only element.

The NuGet package TXTextControl.ReportingCloud is set up in Installing an SDK, and Merge Data explains how the SDK serializes objects.

Handle Levels Without Data

An empty array is an empty block at its level. With the default removeEmptyBlocks setting, true, a sale whose items array is empty keeps its own row, and the content of its items block is removed. A report whose sales array is empty loses the content of the sales block, including the items block inside it. To keep empty blocks, set removeEmptyBlocks to false, as Merge Blocks shows.

Next Steps