TX Text Control .NET Server

Merge Blocks

Topic

Anything that repeats in a document, from the line items of an invoice to a list of contract clauses, is a merge block. A block can also stand for a single object and disappear when the data doesn't contain it. You decide what goes into a block, how blocks are stored in DOCX templates, which kind of block your code inserts, how to list the blocks of a template and how to follow them during a merge.

Merge Blocks in Getting Started introduces blocks: how a block name matches the data, how to create blocks in the Document Editor and with DataSourceManager, and a first sort and filter instruction. Blocks inside blocks are the subject of Nested Blocks. data.json holds the invoice data, and template.tx is the template with a LineItems block that the Getting Started page creates.

What a Merge Block Contains

A merge block can contain a table row, a complete table, paragraphs, images or text frames, and even a single word. MailMerge repeats the whole block for each record, with its formatting:

  • Breaks are repeated. A paragraph end inside the block starts a new paragraph for each record, and a page break, section break or column break in the block is inserted for each record.
  • Each record on a new page. With ParagraphFormat.PageBreakBefore set for the first paragraph of the block, every repetition starts on a new page.
  • Table headers. A header row above a table row block is repeated with the content it had first; Repeat Table Headers per Block gives each block a header of its own.
  • Fields. Merge fields in the block receive the values of the current record. Images and barcodes in a block are merged for each record too.

How Blocks Are Stored

In a document, a merge block is a SubTextPart whose name is txmb_ followed by the block name; MailMerge.MergeBlockNamePrefix returns the prefix. What happens in other formats:

  • DOCX, DOC and RTF templates are loaded with LoadSettings.LoadSubTextParts set to true. It converts bookmarks that extend over several characters into subtext parts, so that the merge blocks are loaded; without it, bookmarks become document targets.
  • Older templates mark a block with two document targets, blockstart_ and blockend_ followed by the block name. They can still be loaded.
  • When saving, ReportingMergeBlockFormat of SaveSettings decides how blocks are written: Default saves them as they are, and DocumentTargets converts subtext part blocks into the older document targets.

Sort and filter instructions are stored with the block in the TX Text Control format, as Sorting and Filtering explains.

Insert Paragraph and Table Row Blocks

DataSourceManager.InsertMergeBlock inserts a block at the input position. The MergeBlockSettings you pass decide how it looks:

PropertySets
BlockTemplateTypeTableRow for a table row block or PlainParagraph for a paragraph block
CreateHeaderRowWhether a table header row is generated for a table row block
HighlightColorThe color that highlights the block in an editor while the input position is inside it
FieldDisplayModeWhether the fields show their text or their field codes
TableIDThe ID of the table that a table row block creates

After the call, AddResult tells whether the block was inserted, and CreatedTable returns the table of a table row block, for example to format it. The following program inserts a paragraph block for the customer of an invoice and merges it:

using TXTextControl;
using TXTextControl.DocumentServer;
using TXTextControl.DocumentServer.DataSources;

string jsonData = File.ReadAllText("data.json");

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    DataSourceManager dataSourceManager = new DataSourceManager();
    dataSourceManager.LoadJson(jsonData);

    // A block for the nested Customer object with its two fields
    MergeBlockInfo customerBlock = new MergeBlockInfo("Customer")
    {
        ColumnNames = new List<string> { "CustomerName", "CustomerAddress" }
    };

    // Paragraphs instead of a table row
    MergeBlockSettings settings = new MergeBlockSettings(BlockTemplateType.PlainParagraph);

    dataSourceManager.InsertMergeBlock(tx, customerBlock, settings);

    if (settings.AddResult != SubTextPartCollection.AddResult.Successful)
    {
        Console.WriteLine($"The merge block was not inserted: {settings.AddResult}");
        return;
    }

    using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
    {
        mailMerge.MergeJsonData(jsonData);
    }

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

The code works as follows:

  • The MergeBlockInfo describes the block: its name, which matches Customer in the data, and in ColumnNames the merge fields it contains.
  • The MergeBlockSettings constructor with BlockTemplateType.PlainParagraph inserts the fields as paragraphs. With BlockTemplateType.TableRow and true as second parameter, it inserts a table with a header row.
  • AddResult is a value of SubTextPartCollection.AddResult, such as Successful or Overlapping.

Use Blocks for Single Objects

A merge block doesn't have to repeat. The Customer block above is bound to a single object: it's merged once when the object exists in the data and removed when it's missing. Include Optional Sections in Conditional Content builds optional sections of a document this way.

List the Blocks of a Template

Before merging a template from another source, you may want to know which blocks and fields it expects. The following program lists them for template.docx:

using TXTextControl;
using TXTextControl.DocumentServer;
using TXTextControl.DocumentServer.DataSources;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    LoadSettings loadSettings = new LoadSettings()
    {
        ApplicationFieldFormat = ApplicationFieldFormat.MSWord,
        LoadSubTextParts = true
    };

    tx.Load("template.docx", StreamType.WordprocessingML, loadSettings);

    using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
    {
        foreach (string blockName in mailMerge.GetBlockNames())
        {
            string[] fieldNames = mailMerge.GetBlockFieldNames(blockName);
            Console.WriteLine($"{blockName}: {string.Join(", ", fieldNames)}");
        }
    }

    foreach (SubTextPart subTextPart in tx.SubTextParts)
    {
        if (DataSourceManager.IsMergeBlock(subTextPart))
        {
            MergeBlockInfo mergeBlockInfo = new MergeBlockInfo(subTextPart);
            Console.WriteLine($"{subTextPart.Name} (level {subTextPart.NestedLevel}): table {mergeBlockInfo.TableName}, {mergeBlockInfo.MergeFields.Length} fields");
        }
    }
}

The code works as follows:

Follow Blocks During the Merge

Two events accompany the merge of a block. BlockMerging occurs before a block is merged, and BlockRowMerged after each of its rows:

using TXTextControl;
using TXTextControl.DocumentServer;

string jsonData = File.ReadAllText("data.json");

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

    using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
    {
        mailMerge.BlockMerging += (sender, e) =>
        {
            Console.WriteLine($"Merging block {e.BlockName} with {e.BlockData?.Length ?? 0} rows");
        };

        mailMerge.BlockRowMerged += (sender, e) =>
        {
            Console.WriteLine($"{e.MergeBlockName}: row {e.DataRowNumber} of {e.DataRowCount}");
        };

        mailMerge.MergeJsonData(jsonData);
    }

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

To change a merged row, load MergedBlockRow into a second ServerTextControl, change the content and assign the saved bytes back. Styling uses this to color every second row, and the blog article Using MailMerge Events to Remove Trailing Text in a Merge Block removes a separator after the last row.

Merge Blocks With a DataSet

To fill merge blocks from a DataSet, name each table like the block it fills, connect it to a master table with a DataRelation and pass the master table to MailMerge.Merge. Merge fills each block with the related table of the same name and the fields outside the blocks with the master table, as Data Sources shows, together with the obsolete MergeBlocks method that older code may call.

Render a Block as a Numbered List

A block doesn't need a table. To merge records as a numbered list, put the merge fields into a numbered list item, add an empty list item after it and make the first item, including its paragraph end, the block. Each record then becomes a numbered item of the list. The blog article Reporting: Merge Blocks Rendered As Numbered Lists shows the template.

Next Steps