TX Text Control .NET for WPF

Merge Events

Topic

Every step of a merge can be observed from your code. MailMerge raises an event before each merge block, after each field, block row and record, and when images, barcodes, charts, form fields and subtemplates are merged. In a WPF application, these are ordinary .NET events of a class that your code creates, not routed events of a XAML element, so handlers are attached with += in MainWindow.xaml.cs. Below you find each event, the order in which they occur, a log that makes the order visible and a handler that cancels a merge.

What the events are used for is shown in context elsewhere: Formatting and Styling colors rows and cells, Merge Fields changes the text of fields, and the Overview saves each record as a document. The examples merge the invoice of the Getting Started tutorials, invoice.docx and invoice.json with its 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

The arguments locate the merged element in the template: MergeBlockName is the name of its merge block, or an empty string outside blocks, and NestingDepth is the level of that block. SubTextPartID and SubTextPartNumber, added in version 33.0, identify the subtext part of the block. The events of images, barcodes and subtemplates are covered in Images and Barcodes and Subtemplates.

The Order of the Events

Within each record of the master table, a merge block and the record raise their events 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 too. A log shows the order for your own template. Add a button and a ListBox to the DockPanel of MainWindow.xaml:

<Button x:Name="logMergeButton" DockPanel.Dock="Top" Content="Merge With Log"
        Click="logMergeButton_Click" />
<ListBox x:Name="mergeLog" DockPanel.Dock="Bottom" Height="150" />

The handler in MainWindow.xaml.cs merges the template that is loaded in textControl1 and writes each event into the list:

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

    using (MailMerge mailMerge = new MailMerge { TextComponent = textControl1 })
    {
        mailMerge.BlockMerging += (s, args) =>
            mergeLog.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}";
                mergeLog.Items.Add($"FieldMerged: {mergeField.Name}{block}, merged: {args.Merged}");
            }
        };

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

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

        // Merge the template in the editor; the result replaces it
        mailMerge.MergeJsonData(File.ReadAllText("invoice.json"));
    }
}

The code works as follows:

The log lists the Items block with its two rows, the fields of each row and the single record, and the editor shows the merged invoice. Load the template again before the next merge.

Cancel a Merge

FieldMerged and DataRowMerged can stop a merge: their arguments have a Cancel property. The following handler merges in a ServerTextControl and saves the PDF only when every field outside the blocks receives data:

using System.IO;
using System.Windows;
using TXTextControl;
using TXTextControl.DocumentServer;
using TXTextControl.DocumentServer.Fields;
private void checkedMergeButton_Click(object sender, RoutedEventArgs 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 cancels the merge at a field, and Cancel of DataRowMergedEventArgs after a 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 without data. To try it, remove Company from invoice.json. BlockMergingEventArgs derives from CancelEventArgs and can cancel the merge of one block.

Change Merged Content

MergedField of FieldMerged, MergedBlockRow of BlockRowMerged and MergedRow of DataRowMerged contain merged content as a byte array in the internal TX Text Control format. A handler loads it into a second ServerTextControl, changes it and assigns the saved bytes back, as Formatting and Styling does with block rows.

The blog article Using MailMerge Events to Remove Trailing Text in a Merge Block uses BlockMerging, FieldMerged and BlockRowMerged together to remove the separator after the last row of a list, and MailMerge: Merging Hyperlinks using the FieldMerged Event creates hyperlinks from merged values.

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 MainWindow.xaml.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, RoutedEventArgs 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 has a Merge method of its own, used for previews, and raises FieldMerged and DataRowMerged with the arguments of the MailMerge events. See Template Preview.

Next Steps