TX Text Control .NET for Windows Forms

Merge Events

Topic

MailMerge reports its progress through events: before a merge block is merged, after each field, each block row and each record, and when images, barcodes, charts, form fields and subtemplates are merged. Handlers of these events check the data, change the merged content, collect results or stop the merge. All events of MailMerge are listed below with the order in which they occur, how a Windows Forms handler is created in the designer and how a handler cancels a merge.

Other pages show what the events are good for: Formatting and Styling colors rows and cells, Merge Fields changes field text, and Overview saves one document per record. The examples use the invoice of the Getting Started tutorials, invoice.docx and invoice.json with the Items block.

The Events of MailMerge

EventOccursIts arguments provide
FieldMergedFor each field, whether it received data or notThe field, Merged, the record, the block, the table cell and the merged field as a byte array; Cancel
BlockMergingBefore a merge block is mergedThe block name, its column names and its rows; Cancel
BlockRowMergedAfter each row of a merge blockThe block name, the row number, the row count and the merged row as a byte array
DataRowMergedAfter each record of the master tableThe record, its number and its merged document as a byte array; Cancel
IncludeTextMergingWhen an INCLUDETEXT field is mergedThe file name and the field; your code can supply the document to include
ImageMergedAfter an image placeholder has been mergedThe image, its name and its descriptive text, which can be replaced
ImageFieldMergedAfter a merge field with the prefix image: has been mergedThe field name and the merged image, which can be replaced
BarcodeMerged, ChartMergedAfter a barcode or a chart has been mergedThe merged BarcodeFrame or ChartFrame and the record
FormFieldMergedAfter a form field has been mergedThe form field, Merged, the record and the table cell

Most arguments tell where in the template an element is: MergeBlockName is the name of its merge block, or an empty string outside blocks, and NestingDepth the level of the block. Since version 33.0, SubTextPartID and SubTextPartNumber identify the subtext part of the block. Images and Barcodes and Subtemplates describe the events of their elements.

The Order of the Events

For each record of the master table, the events of a merge block and the record occur in this order:

Record of the master table
 |- BlockMerging          before a merge block is merged
 |   |- FieldMerged       for each field of a block row
 |   '- BlockRowMerged    after each row of the block
 '- DataRowMerged         after the record has been merged

Fields outside blocks raise FieldMerged as well. To watch the order with your own template, log the events. The following handler of a button named logMergeButton merges the invoice in a ServerTextControl and writes each event into a ListBox named listBox1:

using System;
using System.IO;
using System.Windows.Forms;
using TXTextControl;
using TXTextControl.DocumentServer;
using TXTextControl.DocumentServer.Fields;
private void logMergeButton_Click(object sender, EventArgs e)
{
    listBox1.Items.Clear();

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

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

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

        using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
        {
            mailMerge.BlockMerging += (s, args) =>
                listBox1.Items.Add($"BlockMerging: {args.BlockName}, {args.BlockData.Length} rows");

            mailMerge.FieldMerged += (s, args) =>
            {
                if (args.MailMergeFieldAdapter is MergeField mergeField)
                {
                    string block = args.MergeBlockName == "" ? "" : $" in {args.MergeBlockName}";
                    listBox1.Items.Add($"FieldMerged: {mergeField.Name}{block}, merged: {args.Merged}");
                }
            };

            mailMerge.BlockRowMerged += (s, args) =>
                listBox1.Items.Add($"BlockRowMerged: {args.MergeBlockName} row {args.DataRowNumber} of {args.DataRowCount}");

            mailMerge.DataRowMerged += (s, args) =>
                listBox1.Items.Add($"DataRowMerged: record {args.DataRowNumber}");

            mailMerge.MergeJsonData(File.ReadAllText("invoice.json"));
        }
    }
}

The code works as follows:

The list shows the block Items with its two rows, the fields of each row and one record.

Create Handlers in the Designer

A MailMerge component dropped on the form, mailMerge1 in the component tray, has its events in the Properties window. Select the component, click the Events button with the lightning icon at the top of the window and double-click an event, such as FieldMerged. Visual Studio creates the handler in Form1.cs and connects it in Form1.Designer.cs:

private void mailMerge1_FieldMerged(object sender, MailMerge.FieldMergedEventArgs e)
{
    if (!e.Merged && e.MailMergeFieldAdapter is MergeField mergeField)
    {
        listBox1.Items.Add($"No data for {mergeField.Name}");
    }
}

With its TextComponent set to textControl1, mailMerge1.MergeJsonData(File.ReadAllText("invoice.json")) merges the document in the editor, and the handler runs for each of its fields. Overview shows how to set up the component.

Cancel a Merge

The arguments of FieldMerged and DataRowMerged have a Cancel property that cancels the current merge. The following handler merges the invoice only when every field outside the blocks receives data, and saves no PDF otherwise:

private void checkedMergeButton_Click(object sender, EventArgs e)
{
    string missingField = null;

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

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

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

        using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
        {
            mailMerge.FieldMerged += (s, args) =>
            {
                // Stop at the first field outside a block without data
                if (!args.Merged && args.MergeBlockName == "" &&
                    args.MailMergeFieldAdapter is MergeField mergeField)
                {
                    missingField = mergeField.Name;
                    args.Cancel = true;
                }
            };

            mailMerge.MergeJsonData(File.ReadAllText("invoice.json"));
        }

        if (missingField != null)
        {
            MessageBox.Show(this, $"The merge was canceled: no data for {missingField}.");
            return;
        }

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

Cancel of FieldMergedEventArgs and Cancel of DataRowMergedEventArgs both cancel the merge; the second one fits checks that need the complete record. A merge canceled through DataRowMergedEventArgs.Cancel ends with a MergeCanceledException, which the code that calls the merge method catches. Merged is false for a field that received no data. Remove Company from invoice.json to see the message. BlockMergingEventArgs derives from CancelEventArgs and can cancel the merge of a single block.

Change Merged Content

Three arguments hand over merged content as a byte array in the internal TX Text Control format: MergedField of FieldMerged, MergedBlockRow of BlockRowMerged and MergedRow of DataRowMerged. Load the bytes into a second ServerTextControl, change the content and assign the saved bytes back; the merge continues with the changed content. Formatting and Styling colors block rows this way.

The blog article Using MailMerge Events to Remove Trailing Text in a Merge Block combines BlockMerging, FieldMerged and BlockRowMerged to drop the separator after the last row of a list, and MailMerge: Merging Hyperlinks using the FieldMerged Event turns merged values into hyperlinks.

Skip Records During the Merge

MailMerge merges every record it receives, so the natural place to leave records out is your code that prepares the data. When the decision can only be made during the merge, for example from input of the user, replace the document of a record with an empty one in DataRowMerged. The following handler of Form1.cs merges invoice.json and leaves out the record with the number 1:

using System.Collections.Generic;
using System.IO;
using TXTextControl;
using TXTextControl.DocumentServer;
private void mergeSkippingButton_Click(object sender, EventArgs e)
{
    // The numbers of the records to leave out; the first record has the number 0
    HashSet<int> recordsToSkip = new HashSet<int> { 1 };

    // An empty document that replaces each skipped record
    byte[] emptyDocument;

    using (ServerTextControl empty = new ServerTextControl())
    {
        empty.Create();
        empty.Save(out emptyDocument, BinaryStreamType.InternalUnicodeFormat);
    }

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

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

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

        using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
        {
            mailMerge.DataRowMerged += (s, args) =>
            {
                if (recordsToSkip.Contains(args.DataRowNumber))
                {
                    args.MergedRow = emptyDocument;
                }
            };

            mailMerge.MergeJsonData(File.ReadAllText("invoice.json"));
        }

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

The code works as follows:

  • The empty document is created once, before the merge, and saved in the internal TX Text Control format that MergedRow expects.
  • DataRowNumber counts the records from 0, so the number 1 is the second invoice. Assigning the empty document to MergedRow removes that invoice from the result.
  • To decide by the content of a record instead of its number, read its values from DataRow, as Conditional Content does in FieldMerged.

Add two more invoices to the array of invoice.json to see the effect: invoices.pdf then contains the first and the third invoice. The blog article Mail Merge: Skipping Records During the Merge Process in .NET C# uses the same technique with a list of record numbers.

Events of the DataSourceManager

The DataSourceManager of the Reporting tab merges with its own Merge method, for example for a preview. It raises FieldMerged and DataRowMerged with the same arguments as MailMerge; Template Preview uses this method.

Next Steps