Merge Blocks
Whatever repeats in a merged document, such as the line items of an invoice or the clauses of a contract, comes from a merge block: a part of the template that MailMerge copies once for each record of a list. A block can also stand for a single object and disappear when the data lacks it. Building on the first block of Getting Started, you learn what a block may contain and how it is stored, which block tools the WPF Reporting tab offers, and how your code inserts, checks and inspects blocks in the WPF TextControl.
Add Repeating Merge Blocks in Getting Started inserts the Items block of the invoice template invoice.docx and merges it with invoice.json. The code below extends the same window: textControl1 in MainWindow.xaml, connected to the Reporting tab ribbonReportingTab1, which has invoice.json loaded. Nested Blocks and Master-Detail Reports covers blocks inside blocks.
What a Merge Block Contains
Besides a table row, a block can hold a complete table, paragraphs, images, text frames or a single word. MailMerge copies the whole block with its formatting for each record:
- Breaks are part of the block. A paragraph end in the block starts a new paragraph per record, and a page break, section break or column break in the block is repeated per record.
- A new page per record. When
PageBreakBeforeis set in the paragraph format of the block's first paragraph, each copy starts on a new page; the blog article MailMerge: Starting Each Merge Block on a New Page shows the layout. - Table header rows. A header row above a table row block is repeated at the top of each page. For a header per copy of the block, make the whole table the block; Repeat Table Headers per Block shows why this matters for nested blocks.
- Fields, images and barcodes in the block receive the values of the current record.
How Blocks Are Stored
A merge block is a SubTextPart, a named range of text, whose name starts with txmb_, the value of MailMerge.MergeBlockNamePrefix, followed by the block name. Subtext Parts covers the element in the editor.
- DOCX, DOC and RTF templates are loaded with LoadSettings.LoadSubTextParts set to
true, as in Merge Data into Fields; without it, their blocks aren't imported. - Older templates mark the start and the end of a block with two document targets,
blockstart_andblockend_followed by the block name. This format is still supported. - Saving. ReportingMergeBlockFormat of
SaveSettingskeeps blocks as they are withDefaultor converts them into the older document targets withDocumentTargets.
A block also stores its sorting and filter instructions, as Sorting and Filtering describes.
Work With Blocks in the Reporting Tab
The Merge Blocks group of the WPF Reporting tab inserts a block for a table of the data source through the Insert Merge Block dialog box, makes selected text a custom block and renames or removes blocks with Edit Merge Block. Reporting Ribbon and WPF Dialog Boxes describes these commands and the options of the dialog box.
Check the Result of the Dialog Box
The dialog boxes are WPF windows of the namespace TXTextControl.DocumentServer.WPF: set their Owner and open them with ShowDialog, which returns true when the user clicks OK. After InsertMergeBlockDialog has closed, its properties tell your code what the user inserted. Add a button with Click="insertBlockWithDialogButton_Click" to MainWindow.xaml and this handler to MainWindow.xaml.cs, with using TXTextControl.DocumentServer.DataSources; and using TXTextControl.DocumentServer.WPF;:
private void insertBlockWithDialogButton_Click(object sender, RoutedEventArgs e)
{
DataSourceManager dataSourceManager = ribbonReportingTab1.DataSourceManager;
InsertMergeBlockDialog dialog = new InsertMergeBlockDialog(
dataSourceManager, textControl1, dataSourceManager.DataTables["Items"])
{
Owner = this
};
// ShowDialog returns true when the user clicks OK
if (dialog.ShowDialog() != true)
{
return;
}
if (dialog.AddResult != SubTextPartCollection.AddResult.Successful)
{
MessageBox.Show(this, "The merge block was not inserted: " + dialog.AddResult);
return;
}
// A table row block: format the table the dialog created
if (dialog.BlockType == BlockTemplateType.TableRow && dialog.CreatedTable != null)
{
dialog.CreatedTable.Select();
textControl1.Selection.FontSize = 200; // 10 points in twips
}
}
The code works as follows:
- AddResult is a value of SubTextPartCollection.AddResult:
Successful, or a reason such asOverlappingwhen the new block would overlap another one. - BlockType is the kind of block the user chose, and CreatedTable the table of a table row block.
- MergeBlockInfo describes the inserted block, and HighlightColor the color the user selected.
EditMergeBlocksDialog, the dialog box of Edit Merge Block, is opened the same way with the DataSourceManager and textControl1.
Insert a Paragraph Block in Code
Without a dialog box, DataSourceManager.InsertMergeBlock inserts a block with the MergeBlockSettings you pass:
| Property | Sets |
|---|---|
| BlockTemplateType | TableRow for a table row, PlainParagraph for paragraphs |
| CreateHeaderRow | Whether a table row block gets a header row |
| HighlightColor | The color of the block while the input position is in it, with its alpha value |
| FieldDisplayMode | Whether the fields show their text or their field codes |
The following handler inserts the invoice items as paragraphs instead of table rows. HighlightColor is a System.Drawing.Color, not a WPF color, so the code names its type in full. It needs using System.Collections.Generic;:
private void insertItemListButton_Click(object sender, RoutedEventArgs e)
{
// A paragraph for each item with two of its fields
MergeBlockInfo itemsBlock = new MergeBlockInfo("Items")
{
ColumnNames = new List<string> { "Description", "Quantity" }
};
MergeBlockSettings settings = new MergeBlockSettings(BlockTemplateType.PlainParagraph)
{
// System.Drawing.Color, also in a WPF application
HighlightColor = System.Drawing.Color.FromArgb(64, System.Drawing.Color.SteelBlue)
};
ribbonReportingTab1.DataSourceManager.InsertMergeBlock(textControl1, itemsBlock, settings);
if (settings.AddResult != SubTextPartCollection.AddResult.Successful)
{
MessageBox.Show(this, "The merge block was not inserted: " + settings.AddResult);
}
}
MergeBlockInfo names the block after the Items array, and its ColumnNames become the merge fields. As with the dialog box, AddResult and CreatedTable report the result after the call.
Find the Blocks of a Template
Before a template from a user goes into production, your application can check which blocks it expects. A MailMerge connected to the WPF TextControl lists them without merging:
private void listBlocksButton_Click(object sender, RoutedEventArgs e)
{
StringBuilder report = new StringBuilder();
using (MailMerge mailMerge = new MailMerge { TextComponent = textControl1 })
{
foreach (string blockName in mailMerge.GetBlockNames())
{
string[] fieldNames = mailMerge.GetBlockFieldNames(blockName);
report.AppendLine(blockName + ": " + string.Join(", ", fieldNames));
}
}
MessageBox.Show(this, report.ToString(), "Merge blocks");
}
With using System.Text; and using TXTextControl.DocumentServer;, the handler shows Items: Description, Quantity, Price for the invoice template. GetBlockNames returns the block names and GetBlockFieldNames the fields of one block.
To find out which block the user is working in, ask for the subtext part at the input position with SubTextParts.GetItem:
private void currentBlockButton_Click(object sender, RoutedEventArgs e)
{
// The innermost subtext part at the input position, or null
SubTextPart subTextPart = textControl1.SubTextParts.GetItem();
if (subTextPart == null || !DataSourceManager.IsMergeBlock(subTextPart))
{
MessageBox.Show(this, "The input position is not in a merge block.");
return;
}
MergeBlockInfo mergeBlockInfo = new MergeBlockInfo(subTextPart);
MessageBox.Show(this,
$"Block {mergeBlockInfo.TableName}, level {subTextPart.NestedLevel}, " +
$"{mergeBlockInfo.MergeFields.Length} merge fields");
}
DataSourceManager.IsMergeBlock separates merge blocks from other subtext parts. A MergeBlockInfo created from a subtext part reads an existing block: TableName is the name of its data, and MergeFields its fields without those of nested blocks. NestedLevel is 1 at the top level.
Rename Blocks and Fields in Code
Block names and prefixed field names must follow the data. When a table or an array of the data gets a new name, every template that uses it needs the new name too. Edit Merge Block of the WPF Reporting tab renames the blocks of one template; for many templates, your code renames them. A block is a subtext part named txmb_ and the block name, and a field of a related table carries the table name before the dot, so both are renamed with string operations. The following method of MainWindow.xaml.cs renames a table in the template in textControl1:
using TXTextControl;
using TXTextControl.DocumentServer;
using TXTextControl.DocumentServer.Fields;
private void RenameDataTable(string oldName, string newName)
{
string oldBlockName = MailMerge.MergeBlockNamePrefix + oldName;
// The main text, text frames, headers and footers
foreach (IFormattedText textPart in textControl1.TextParts)
{
// Merge blocks are subtext parts named txmb_ and the block name
foreach (SubTextPart subTextPart in textPart.SubTextParts)
{
if (subTextPart.Name == oldBlockName)
{
subTextPart.Name = MailMerge.MergeBlockNamePrefix + newName;
}
}
// Merge fields that address the table with dot notation, such as Items.Price
foreach (ApplicationField applicationField in textPart.ApplicationFields)
{
if (applicationField.TypeName != "MERGEFIELD")
{
continue;
}
MergeField mergeField = new MergeField(applicationField);
if (mergeField.Name.StartsWith(oldName + "."))
{
mergeField.Name = newName + mergeField.Name.Substring(oldName.Length);
}
}
}
}
The code works as follows:
- TextParts holds the main text, the text frames and the headers and footers. Each part has its own SubTextParts and
ApplicationFields. - A subtext part whose Name is
txmb_Itemsis the blockItems. SettingNamerenames the block. - A
MergeFieldwrapped around each merge field changes Name in the document. Fields inside the block need no change, because their names don't contain the table name.
RenameDataTable("Items", "LineItems") adapts the invoice template to data whose array is named LineItems. Save the template afterward. The blog article Renaming Merge Blocks and Merge Fields Programmatically in C# wraps the same steps in a helper class.
Use a Block for a Single Object
When a block name matches a single nested object instead of an array, the block is merged once if the object exists. When the object or the array is missing from the data, the block is removed with its content, because RemoveEmptyBlocks is true by default. Conditional Content builds optional sections on this behavior.
Follow Blocks During the Merge
BlockMerging occurs before a block is merged and passes its data, and BlockRowMerged occurs after each merged row, which your code can change. Both are described in Merge Events, and Formatting and Styling uses them to format rows.
Merge Blocks From a DataSet
A DataSet fills blocks through the relations between its tables, when MailMerge.Merge is called with the master table. DataSets and DataTables explains the setup and what replaces the obsolete MergeBlocks method.
Render a Block as a Numbered List
Records can become the items of a numbered list. Put the merge fields into a numbered list item, add an empty list item below it, select from the fields to the empty item, so that the paragraph end of the first item is included, and make the selection a block. The blog article Reporting: Merge Blocks Rendered As Numbered Lists walks through such a template.
Next Steps
- Nested Blocks and Master-Detail Reports: blocks inside blocks for orders and their items.
- Sorting and Filtering: merge the rows of a block in order and only the rows you need.
- Conditional Content: include or leave out sections depending on the data.
- Template Preview: show users the merged blocks while they design.