# Nested Blocks and Master-Detail Reports

> Nest merge blocks for master-detail reports in a WPF app: nested JSON, ChildBlocks in code, the block hierarchy, business objects and DataSets with relations.

- **Product:** TX Text Control .NET for WPF
- **Section:** Documentation
- **Kind:** Topic
- **Path:** TX Text Control .NET for WPF > Mail Merge and Reporting > Nested Blocks and Master-Detail Reports
- **5744 words**
- **Web URL:** https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/nested-blocks/
- **LLMs URL:** https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/nested-blocks/llms.txt
- **LLMs-Full URL:** https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/nested-blocks/llms-full.txt

---

A report of orders with their articles or of departments with their employees needs a template with levels: an outer merge block that is repeated for each master record and, inside it, an inner block for the detail records of the current master. Such master-detail reports are built from the same merge blocks as a simple invoice. You shape the nested data, design the levels in the WPF Reporting tab or in code, check the hierarchy of a template, merge JSON, objects or a `DataSet`, and decide what happens to levels without data.

[Merge Blocks](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/blocks/llms-full.txt) describes blocks at one level, and [Add Repeating Merge Blocks](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/getting-started/mail-merge/blocks/llms-full.txt) the first block of the invoice. The handlers below go into *MainWindow.xaml.cs* of the same WPF application, next to the `TextControl` `textControl1`; add a button to *MainWindow.xaml* for each, with its `Click` attribute set to the handler's name.

### How Nested Blocks Work

The master-detail relationship is one-to-many: one article group has many articles. In this template, the whole table is the outer block for the groups, and its last row, highlighted darker, is the inner block for the articles:

![A template with an outer merge block around a table for article groups and an inner merge block in its last row for the articles.](https://docs.textcontrol.com/assets/img/tx-text-control-dotnet-wpf/nested-blocks-template.6b09be45.webp)

The merge creates a table for each group, with the articles of that group:

![The merged document with a table for the article group Vegetables and one for Fruits, each listing its articles with description and price.](https://docs.textcontrol.com/assets/img/tx-text-control-dotnet-wpf/nested-blocks-merged.1024efa1.webp)

- **Each block is named like its data**, the array, collection or table that holds its records.
- **Fields take the values of the record of their block.** `article_name` in the inner block is the name of the current article; fields of the group belong outside it.
- **An inner block lies completely inside its outer block.** Blocks can't overlap, but two blocks may start or end at the same position.
- **Blocks nest to any depth**, because `MailMerge` processes the hierarchy recursively.

In a `DataSet`, a relation between the tables connects the levels, here the column `article_group_id`:

![Two tables, Group and Article, connected by a relation from article_group_id in Article to article_group_id in Group.](https://docs.textcontrol.com/assets/img/tx-text-control-dotnet-wpf/nested-blocks-data-relation.3a2ddd3b.webp)

### Shape the Nested Data

In JSON, a nested array becomes a nested block. Add *orders.json* to the project, with **Copy to Output Directory** set to **Copy if newer**. It holds a company with two orders, each with its articles:

```json
[
  {
    "company_name": "Text Control, LLC",
    "orders": [
      {
        "id": 123,
        "articles": [
          { "id": 1, "product": { "name": "Product A", "price": 200 }, "qty": 5 }
        ]
      },
      {
        "id": 321,
        "articles": [
          { "id": 2, "product": { "name": "Product B", "price": 50 }, "qty": 12 }
        ]
      }
    ]
  }
]
```

The template for this data has three levels:

| Level | Template element | Fields |
|---|---|---|
| Root | Merge fields outside all blocks | `company_name` |
| 1 | Merge block `orders` | `id` |
| 2 | Merge block `articles`, inside `orders` | `product.name`, `qty` |

The field `product.name` addresses the nested `product` object of an article with dot notation; [Merge Fields](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/fields/llms-full.txt) explains the notation.

### Design Nested Blocks in the Reporting Tab

In the WPF Reporting tab, users insert the levels from the outside in, with the data loaded into the tab:

1. Insert the outer block, `orders`, from the **Insert Merge Block** drop-down, for example as a table row with a header row.
2. Place the input position inside that block, for example in a row added to its table, and insert the inner block, `articles`.
3. Open the field navigator with **Field Navigation**. Its **Merge Blocks** tab shows the blocks as a tree, the inner block below the outer one.

The field navigator needs a `Sidebar` connected to the tab, as [Create a Template](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/getting-started/mail-merge/template/#show-the-field-navigation-sidebar) sets up. The blog article [Creating Nested Repeating Blocks with Version X13 (23.0)](https://www.textcontrol.com/blog/2015/12/15/creating-nested-repeating-blocks-with-version-x13-230/) builds a nested template step by step in TX Words.

### Create Nested Blocks in Code

The blocks inside a block are listed in [MergeBlockInfo.ChildBlocks](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver-datasources/mergeblockinfo/childblocks-property/llms-full.txt). `InsertMergeBlock` inserts a block with all its child blocks in one call. The handler needs `using System.Collections.Generic;`, `using System.IO;` and `using TXTextControl.DocumentServer.DataSources;`:

```csharp
private void createOrdersTemplateButton_Click(object sender, RoutedEventArgs e)
{
    string jsonData = File.ReadAllText("orders.json");

    // A DataSourceManager of its own, so the Reporting tab keeps its data
    DataSourceManager dataSourceManager = new DataSourceManager();
    dataSourceManager.LoadJson(jsonData);

    // Outer block: repeated for each order
    MergeBlockInfo ordersBlock = new MergeBlockInfo("orders")
    {
        ColumnNames = new List<string> { "id" },

        // Inner block: repeated for each article of the current order
        ChildBlocks = new List<MergeBlockInfo>
        {
            new MergeBlockInfo("articles")
            {
                ColumnNames = new List<string> { "product.name", "qty" }
            }
        }
    };

    dataSourceManager.InsertMergeBlock(textControl1, ordersBlock,
        new MergeBlockSettings(BlockTemplateType.TableRow, true));

    textControl1.Save("orders-template.tx", StreamType.InternalUnicodeFormat);
}
```

The code works as follows:

- [LoadJson](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver-datasources/datasourcemanager/loadjson-method/llms-full.txt) reads the structure of the data, nested arrays included.
- Each [MergeBlockInfo](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver-datasources/mergeblockinfo/llms-full.txt) is one level, with its name and its merge fields in `ColumnNames`.
- [InsertMergeBlock](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver-datasources/datasourcemanager/insertmergeblock-method/llms-full.txt) creates the tables, fields and blocks of both levels at the input position of the WPF `TextControl`.
- The template is saved as *orders-template.tx*, which the following examples merge.

### Check the Block Hierarchy

A `MergeBlockInfo` read from an existing block contains its child blocks. The following handler walks the tree and shows it indented; it needs `using System.Text;`:

```csharp
private void showHierarchyButton_Click(object sender, RoutedEventArgs e)
{
    StringBuilder report = new StringBuilder();

    foreach (SubTextPart subTextPart in textControl1.SubTextParts)
    {
        // Start with the merge blocks at the top level
        if (DataSourceManager.IsMergeBlock(subTextPart) && subTextPart.NestedLevel == 1)
        {
            AppendBlock(report, new MergeBlockInfo(subTextPart), 0);
        }
    }

    MessageBox.Show(this, report.ToString(), "Block hierarchy");
}

// Adds a block and, indented, its child blocks
private static void AppendBlock(StringBuilder report, MergeBlockInfo block, int depth)
{
    report.AppendLine(new string(' ', depth * 4) + block.TableName);

    if (block.ChildBlocks == null)
    {
        return;
    }

    foreach (MergeBlockInfo child in block.ChildBlocks)
    {
        AppendBlock(report, child, depth + 1);
    }
}
```

[SubTextPart.NestedLevel](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol/subtextpart/nestedlevel-property/llms-full.txt) is 1 for the blocks at the top level, and [TableName](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver-datasources/mergeblockinfo/tablename-property/llms-full.txt) is the name of a block's data. With the orders template in the editor, the message box lists `orders` and, indented below it, `articles`.

### Merge Nested Business Objects

With business objects, a collection property fills the block of the same name on every level. Add these classes to the project:

```csharp
using System.Collections.Generic;

public class Company
{
    public string company_name { get; set; }
    public List<Order> orders { get; set; } = new List<Order>();
}

public class Order
{
    public int id { get; set; }
    public List<Article> articles { get; set; } = new List<Article>();
}

public class Article
{
    public int id { get; set; }
    public Product product { get; set; }
    public int qty { get; set; }
}

public class Product
{
    public string name { get; set; }
    public decimal price { get; set; }
}
```

This handler merges a company into the template in the editor; it needs `using TXTextControl.DocumentServer;`:

```csharp
private void mergeOrdersButton_Click(object sender, RoutedEventArgs e)
{
    Company company = new Company { company_name = "Text Control, LLC" };

    Order order = new Order { id = 123 };
    order.articles.Add(new Article { id = 1, qty = 5, product = new Product { name = "Product A", price = 200 } });
    company.orders.Add(order);

    using (MailMerge mailMerge = new MailMerge { TextComponent = textControl1 })
    {
        // The template in the editor is replaced with the merged document
        mailMerge.MergeObject(company);
    }
}
```

[MergeObject](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver/mailmerge/mergeobject-method/llms-full.txt) merges one object and [MergeObjects](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver/mailmerge/mergeobjects-method/llms-full.txt) a collection. The rules for business objects are in [Objects and Collections](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/data-sources/objects/llms-full.txt).

### Merge a DataSet With Relations

A `DataSet` defines its levels with relations: each block is named like a `DataTable`, and a `DataRelation` connects the table to the table of the outer block. [Merge](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver/mailmerge/merge-method/llms-full.txt) with the master table follows the relations. This handler merges *orders-template.tx* in the background and saves a PDF; it needs `using System.Data;`:

```csharp
private void mergeDataSetButton_Click(object sender, RoutedEventArgs e)
{
    DataSet dataSet = new DataSet();

    DataTable company = dataSet.Tables.Add("company");
    company.Columns.Add("company_id", typeof(int));
    company.Columns.Add("company_name");
    company.Rows.Add(1, "Text Control, LLC");

    DataTable orders = dataSet.Tables.Add("orders");
    orders.Columns.Add("company_id", typeof(int));
    orders.Columns.Add("id", typeof(int));
    orders.Rows.Add(1, 123);
    orders.Rows.Add(1, 321);

    DataTable articles = dataSet.Tables.Add("articles");
    articles.Columns.Add("order_id", typeof(int));
    articles.Columns.Add("product_id", typeof(int));
    articles.Columns.Add("qty", typeof(int));
    articles.Rows.Add(123, 1, 5);
    articles.Rows.Add(321, 2, 12);

    DataTable product = dataSet.Tables.Add("product");
    product.Columns.Add("product_id", typeof(int));
    product.Columns.Add("name");
    product.Columns.Add("price", typeof(decimal));
    product.Rows.Add(1, "Product A", 200m);
    product.Rows.Add(2, "Product B", 50m);

    // The relations connect each block with the rows of its parent
    dataSet.Relations.Add(new DataRelation("company_orders",
        company.Columns["company_id"], orders.Columns["company_id"]));
    dataSet.Relations.Add(new DataRelation("order_articles",
        orders.Columns["id"], articles.Columns["order_id"]));

    // Fields with the prefix product. reach the product of an article
    dataSet.Relations.Add(new DataRelation("article_product",
        articles.Columns["product_id"], product.Columns["product_id"], false));

    using (ServerTextControl tx = new ServerTextControl())
    {
        tx.Create();
        tx.Load("orders-template.tx", StreamType.InternalUnicodeFormat);

        using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
        {
            // Merge the master table; the related tables fill the nested blocks
            mailMerge.Merge(company, true);
        }

        tx.Save("orders.pdf", StreamType.AdobePDF);
    }
}
```

The code works as follows:

- The relations `company_orders` and `order_articles` connect each block with the rows of its parent, and the tables are named like the blocks `orders` and `articles`.
- Inside the `articles` block, `qty` has no prefix, because it is a column of the block's own table. `product.name` has the prefix `product`: a prefix inside a block starts at the table of the block and follows the relation `article_product` to the product row of the current article, so the field shows "Product A" in order 123 and "Product B" in order 321.
- `false`, the last parameter of the `DataRelation` constructor, creates the relation without constraints. With constraints, the `DataSet` would require a unique `product_id` in `articles`, and two articles of the same product couldn't be added.

Longer prefixes follow more than one relation, as the multi-level example in [Merge Fields](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/fields/#prefixes-in-field-names) shows. **Edit Data Relations** in the **Data Source** group of the Reporting tab lets users add or remove the relations of the loaded data source; [DataSets and DataTables](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/data-sources/datasets/llms-full.txt) covers relations in depth.

### Repeat Table Headers per Block

A header row repeats at the top of each page with its first content. If an inner table row block sits below a header that shows data of the outer block, such as the order number, every page shows the first order. Make the whole table, header row included, part of the outer block, and each order gets a table with a header of its own. The blog article [MailMerge: Table Headers and Repeating Blocks](https://www.textcontrol.com/blog/2017/08/01/mailmerge-table-headers-and-repeating-blocks/) shows both layouts.

### Handle Levels Without Data

[RemoveEmptyBlocks](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver/mailmerge/removeemptyblocks-property/llms-full.txt) is `true` by default and applies to all levels: an order without articles keeps its `orders` block and loses its `articles` block, and an outer block without data is removed together with the blocks inside it.

A [BlockMergingCondition](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/reference/txtextcontrol-documentserver-datasources/mergeblockinfo/blockmergingcondition-property/llms-full.txt) on the inner block merges it only for parent records that match, for example orders above an amount. [Sorting and Filtering](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/sorting-filtering/#merge-a-block-only-for-matching-parent-rows) sets one.

### Next Steps

- [Sorting and Filtering](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/sorting-filtering/llms-full.txt): sort and filter the rows of each level.
- [Data Sources](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/data-sources/llms-full.txt): JSON, objects and DataSets as merge data.
- [Template Preview](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/template-preview/llms-full.txt): check the merged levels while designing.

---

## Nearby

- **Previous:** [Merge Blocks](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/blocks/llms-full.txt)
- **Next:** [Subtemplates](https://docs.textcontrol.com/tx-text-control-dotnet-wpf/mail-merge/subtemplates/llms-full.txt)
