Merge Blocks
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.PageBreakBeforeset 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_andblockend_followed by the block name. They can still be loaded. - When saving, ReportingMergeBlockFormat of
SaveSettingsdecides how blocks are written:Defaultsaves them as they are, andDocumentTargetsconverts 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:
| Property | Sets |
|---|---|
| BlockTemplateType | TableRow for a table row block or PlainParagraph for a paragraph block |
| CreateHeaderRow | Whether a table header row is generated for a table row block |
| HighlightColor | The color that highlights the block in an editor while the input position is inside it |
| FieldDisplayMode | Whether the fields show their text or their field codes |
| TableID | The 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
Customerin the data, and in ColumnNames the merge fields it contains. - The MergeBlockSettings constructor with
BlockTemplateType.PlainParagraphinserts the fields as paragraphs. WithBlockTemplateType.TableRowandtrueas second parameter, it inserts a table with a header row. AddResultis a value of SubTextPartCollection.AddResult, such asSuccessfulorOverlapping.
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:
- GetBlockNames returns the names of all merge blocks of the loaded template, and GetBlockFieldNames the names of the merge fields in a block.
- DataSourceManager.IsMergeBlock tells whether a subtext part is a merge block.
- The MergeBlockInfo constructor with a subtext part reads an existing block. TableName is the name of its data table, and MergeFields contains its merge fields without those of nested blocks.
- SubTextPart.NestedLevel is 1 for a block at the top level.
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);
}
- BlockMergingEventArgs has the BlockName, the ColumnNames and the rows of the block in BlockData.
- BlockRowMergedEventArgs has the MergeBlockName, the DataRowNumber of the merged row, the DataRowCount of the block and the merged row itself in MergedBlockRow, a byte array in the TX Text Control format.
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
- Nested Blocks: merge master-detail data with blocks inside blocks.
- Sorting and Filtering: merge the rows of a block in order and only the rows you need.
- Styling: format merged rows.
- Subtext Parts: the document element that blocks are made of.