Back to Devexpress

Mail Merge in Rich Text Editor

wpf-9110-controls-and-libraries-rich-text-editor-mail-merge.md

latest37.7 KB
Original Source

Mail Merge in Rich Text Editor

  • Nov 05, 2025
  • 18 minutes to read

This topic describes the mail merge functionality available in the Rich Text Editor for WPF. You can generate or load document templates, insert merge fields, merge plain or master-detail data, and export the result to supported formats. Specially-designed events allow you to control the mail merge process.

Mail merge involves the following steps:

Create or Load a Mail Merge Template

A template document is the starting point. It can be an existing DOCX file with placeholder text or a blank file you create in code. It contains merge fields and region markers. During the merge, RichEditControl replaces fields with actual data from your data source.

Call the RichEditControl.LoadDocument or RichEditControl.LoadDocumentTemplate method to load a document template.

Tip

Subscribe to the DocumentLoaded event to perform safe post-load modifications.

The following code snippet loads the document template:

csharp
using DevExpress.Office.Services;
using DevExpress.Xpf.RichEdit;
using DevExpress.XtraRichEdit.API.Native;

richEditControl.LoadDocument("template.docx");
//...
vb
Imports DevExpress.Office.Services
Imports DevExpress.Xpf.RichEdit
Imports DevExpress.XtraRichEdit.API.Native

richEditControl.LoadDocument("template.docx")
'...

Define Master-Detail Regions in a Template

The DevExpress WPF Rich Text Editor allows you to build reports. In a report, data from multiple records is displayed sequentially as table rows. You can even build a master-detail report. Use TableStart:RegionName and TableEnd:RegionName merge fields to mark regions that contain data from a single record (master or detail). The region name should match the group or table name in your data source. The following image shows a sample master-detail template:

Call the GetRegionHierarchy() method to inspect the nested region structure. This method returns a MailMergeRegionInfo object. The Regions collection contains first level regions. Each object in the collection stores its own Regions.

The following code snippet calls the GetRegionHierarchy() method for the above-metioned template. The console shows the following structure:

csharp
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
//...

// Load the template into the RichEditControl instance.
richEditControl.LoadDocument(templatePath);

var regionInfo = richEditControl.Document.GetRegionHierarchy();

Console.WriteLine(String.Format($"Regions found: {regionInfo.Regions.Count}; " +
    $"\r\n Main region: {regionInfo.Regions[0].Name} " +
    $"\r\n Detail region: {regionInfo.Regions[0].Regions[0].Name}"));
Console.ReadKey();
vb
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native
Imports System.IO

' Load the template into the word processor instance.
richEditControl.LoadDocument(templatePath)

Dim regionInfo = richEditControl.Document.GetRegionHierarchy()
Console.WriteLine(String.Format("Regions found: {0}; " &
                                vbCrLf & " Main region: {1} " &
                                vbCrLf & " Detail region: {2}",
                                regionInfo.Regions.Count,
                                regionInfo.Regions(0).Name,
                                regionInfo.Regions(0).Regions(0).Name))
Console.ReadKey()

Consider the following rules when organizing master-detail regions. RichEditControl throws exceptions if any of these rules are not met.

  • The parent region should enclose the detail region.
  • If a region is located in a table, both start and end fields should be within this table.
  • A single paragraph cannot contain both the start and end region fields and/or multiple regions.

Add Fields to the Template

The FieldCollection.Create method allows you to add fields to a template. Use these field types to populate a template with data, dynamic content, or images during mail merge:

FieldDescriptionSupports Formatted ContentSupports Image Insertion
MERGEFIELDInserts plain text from a data source column.NoNo
DOCVARIABLEInserts dynamically generated or formatted content supplied in the CalculateDocumentVariable event.Yes (paragraphs, formatted runs, tables)Yes
INCLUDEPICTUREInserts an image from a file path, database, or external source (HTML/MHT with URIs).N/AYes

The following example inserts MERGEFIELD fields in the detail table. The Discount merge field is nested inside a formula field so the discount is calculated as a percentage.

csharp
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
//...

// Build a path to the DOCX template file relative to the project's base directory.
var templatePath = Path.Combine(
    AppDomain.CurrentDomain.BaseDirectory,
    "data",
    "template.docx"
);

richEditControl.LoadDocument(templatePath);
GenerateMergeFields(richEditControl.Document);

void GenerateMergeFields(Document document)
{
    // Obtain the detail table (the second table in the document)
    Table detailTable = document.Tables[1];
    // Create the Quantity field
    document.Fields.Create(
        detailTable.Rows[2].Cells[1].Range.Start,
        "MERGEFIELD Quantity"
    );

    // Create the formula field to calculate Discount as a percentage
    Field discountField = document.Fields.Create(
        detailTable.Rows[2].Cells[3].Range.Start,
        "= { placeholder }*100 \\#0%"
    );

    // Find a placeholder for a nested MERGEFIELD
    DocumentRange nestedFieldRange = document.FindAll(
        "{ placeholder }",
        SearchOptions.WholeWord,
        discountField.CodeRange
    ).First();

    // Clear the placeholder range
    document.Delete(nestedFieldRange);

    // Create a nested DISCOUNT field
    document.Fields.Create(nestedFieldRange.Start, "MERGEFIELD Discount");
}
//...
vb
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native
'...

' Build a path to the DOCX template file relative to the project's base directory.
Dim templatePath As String = Path.Combine( _
    AppDomain.CurrentDomain.BaseDirectory, _
    "data", _
    "template.docx" _
)

richEditControl.LoadDocument(templatePath)
GenerateMergeFields(richEditControl.Document)

Sub GenerateMergeFields(document As Document)
    ' Obtain the detail table (the second table in the document)
    Dim detailTable As Table = document.Tables(1)

    ' Create the Quantity field
    document.Fields.Create( _
        detailTable.Rows(2).Cells(1).Range.Start, _
        "MERGEFIELD Quantity" _
    )

    ' Create the formula field to calculate Discount as a percentage
    Dim discountField As Field = document.Fields.Create( _
        detailTable.Rows(2).Cells(3).Range.Start, _
        "= { placeholder }*100 \#0%" _
    )

    ' Find a placeholder for a nested MERGEFIELD
    Dim nestedFieldRange As DocumentRange = document.FindAll( _
        "{ placeholder }", _
        SearchOptions.WholeWord, _
        discountField.CodeRange _
    ).First()

    ' Clear the placeholder range
    document.Delete(nestedFieldRange)

    ' Create a nested DISCOUNT field
    document.Fields.Create(nestedFieldRange.Start, "MERGEFIELD Discount")
End Sub
'...

Insert Dynamic Content

The DOCVARIABLE field allows you to insert any type of content into a document – from a simple variable to another document’s content. Variables can be stored in a document or calculated within the RichEditControl.CalculateDocumentVariable event. Refer to the following examples for more information:

Insert Images from the Database

You can insert images during mail merge with the INCLUDEPICTURE field or the DOCVARIABLE field.

Use INCLUDEPICTURE for simple static paths or when you need a raw image with minimal formatting. Use DOCVARIABLE when you need to compute the image path dynamically, resize or format the image, or insert descriptive text alongside the image.

Example: Use DOCVARIABLE to Insert an Image from a DataSet

Handle the RichEditControl.CalculateDocumentVariable event to insert an image from a DataSet.

csharp
using System.Data;
using System.IO;
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;

// Subscribe to the CalculateDocumentVariable event
richEditControl.CalculateDocumentVariable += (sender, e) => 
    RichEditControl_CalculateDocumentVariable(sender, e, dataSet);

static void RichEditControl_CalculateDocumentVariable(
    object sender,
    CalculateDocumentVariableEventArgs e,
    DataSet dataSet
) {
    // Ensure arguments exist.
    if (e.Arguments.Count == 0)
        return;

    switch (e.VariableName) {
        // Process "Photo" DOCVARIABLE (embed inline image from Base64).
        case "Photo": {
            string id = e.Arguments[0].Value.ToString();
            // Exit the loop early if IDs are empty/invalid.
            if (id.Trim() == string.Empty || id.Contains("<")) {
                e.Value = " ";
                e.Handled = true;
                return;
            }

            // Look up the employee record and decode the image.
            DataTable employeesTable = dataSet.Tables[0];
            DataRow? row = employeesTable.Rows.Find(Convert.ToInt32(id));
            string? imageData = row?["Photo"] as string;

            if (imageData != null) {
                var imageProcessor = new RichEditDocumentServer();
                byte[] imageBytes = Convert.FromBase64String(imageData);

                Shape image = imageProcessor.Document.Shapes.InsertPicture(
                    imageProcessor.Document.Range.Start,
                    DocumentImageSource.FromStream(new MemoryStream(imageBytes))
                );
                image.TextWrapping = TextWrappingType.InLineWithText;

                // Set the RichEditDocumentServer with prepared content as the field value.
                e.Value = imageProcessor;
                e.Handled = true;
            } else {
                // No image found.
                e.Value = null;
            }
            break;
        }
    }
}
vb
Imports System.Data
Imports System.IO
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native

' Subscribe to the CalculateDocumentVariable event
AddHandler richEditControl.CalculateDocumentVariable, Sub(sender, e) 
RichEditControl_CalculateDocumentVariable(sender, e, dataSet)

Private Shared Sub RichEditControl_CalculateDocumentVariable( _
    sender As Object, _
    e As CalculateDocumentVariableEventArgs, _
    dataSet As DataSet _
)
    ' Ensure arguments exist.
    If e.Arguments.Count = 0 Then Return

    Select Case e.VariableName
        ' Process "Photo" DOCVARIABLE (embed inline image from Base64).
        Case "Photo"
            Dim id As String = e.Arguments(0).Value.ToString()
            ' Exit the loop early if IDs are empty/invalid.
            If id.Trim() = String.Empty OrElse id.Contains("<") Then
                e.Value = " " : e.Handled = True : Return
            End If

            ' Look up the employee record and decode the image.
            Dim employeesTable As DataTable = dataSet.Tables(0)
            Dim row As DataRow = employeesTable.Rows.Find( _
                Convert.ToInt32(id) _
            )
            Dim imageData As String = If( _
                row Is Not Nothing, _
                TryCast(row("Photo"), String), _
                Nothing _
            )

            If imageData IsNot Nothing Then
                Dim imageProcessor As New RichEditDocumentServer()
                Dim imageBytes As Byte() = Convert.FromBase64String(imageData)

                Dim image As Shape = imageProcessor.Document.Shapes.InsertPicture( _
                    imageProcessor.Document.Range.Start, _
                    DocumentImageSource.FromStream(New MemoryStream(imageBytes)) _
                )
                image.TextWrapping = TextWrappingType.InLineWithText

                ' Set the RichEditDocumentServer with prepared content as the field value.
                e.Value = imageProcessor
                e.Handled = True
            Else
                ' No image found.
                e.Value = Nothing
            End If
    End Select
End Sub

Example: Use INCLUDEPICTURE Field to Insert an Image from the Database

Use the IUriStreamProvider implementation to ensure that you load images and not just references to these images. This applies to images loaded from:

  • A database during the mail merge process
  • An external source when an HTML or MHT file contains URIs to these images

Create the IUriStreamProvider interface implementation, and customize its GetStream(String) method. This method returns a stream with image data.

The the following code snippet shows the IUriStreamProvider implementation used to insert images from a database. The INCLUDEPICTURE field in the template has a nested MERGEFIELD that refers to the EmployeeID field from the database. The IUriStreamProvider.GetStream method parses the received URI (the INCLUDEPICTURE field), finds the required data row, and returns the MemoryStream with an image.

Note

Make sure that the MERGEFIELD field nested in the INCLUDEPICTURE field refers to the data table’s primary key. Otherwise, the IUriStreamProvider service cannot correctly find the required table row.

csharp
using DevExpress.Office.Services;
using System;
using System.Data;
using System.IO;

public class ImageStreamProvider : IUriStreamProvider
{
    static readonly string prefix = "dbimg://";
    DataTable table;
    string columnName;

    public ImageStreamProvider(DataTable sourceTable, string imageColumn)
    {
        this.table = sourceTable;
        this.columnName = imageColumn;
    }

    public Stream GetStream(string uri)
    {
        // Parse the retrieved URI string
        uri = uri.Trim();
        if (!uri.StartsWith(prefix))
            return null;

        // Remove the prefix from the retrieved URI string
        string strId = uri.Substring(prefix.Length).Trim();
        int id;

        // Check if the string contains the primary key
        if (!int.TryParse(strId, out id))
            return null;

        // Retrieve the row that corresponds
        // with the key
        DataRow row = table.Rows.Find(id);
        if (row == null)
            return null;

        // Convert the image string from this row
        // to a byte array
        byte[] bytes = Convert.FromBase64String(row[columnName] as string) as byte[];
        if (bytes == null)
            return null;

        // Return the MemoryStream with an image
        MemoryStream memoryStream = new MemoryStream(bytes);
        return memoryStream;
    }
}
vb
Imports DevExpress.Office.Services
Imports System
Imports System.Data
Imports System.IO

Public Class ImageStreamProvider
    Inherits IUriStreamProvider

    Shared ReadOnly prefix As String = "dbimg://"
    Private table As DataTable
    Private columnName As String

    Public Sub New(ByVal sourceTable As DataTable, ByVal imageColumn As String)
        Me.table = sourceTable
        Me.columnName = imageColumn
    End Sub

    Public Function GetStream(ByVal uri As String) As Stream
        uri = uri.Trim()
        If Not uri.StartsWith(prefix) Then Return Nothing
        Dim strId As String = uri.Substring(prefix.Length).Trim()
        Dim id As Integer
        If Not Integer.TryParse(strId, id) Then Return Nothing
        Dim row As DataRow = table.Rows.Find(id)
        If row Is Nothing Then Return Nothing
        Dim bytes As Byte() = TryCast(Convert.FromBase64String(TryCast(row(columnName), String)), Byte())
        If bytes Is Nothing Then Return Nothing
        Dim memoryStream As MemoryStream = New MemoryStream(bytes)
        Return memoryStream
    End Function
End Class

Call the IUriStreamService.RegisterProvider method to register the provider class.

csharp
using DevExpress.Office.Services;
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;

//...
IUriStreamService uriStreamService = richEditControl.GetService<IUriStreamService>();
uriStreamService.RegisterProvider(new ImageStreamProvider(xmlDataSet.Tables[0], "Photo"));
vb
Imports DevExpress.Office.Services
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native

' ...
' Obtain the IUriStreamService from the RichEditDocumentServer.
Dim uriStreamService As IUriStreamService = richEditControl.GetService(Of IUriStreamService)()

' Register the custom ImageStreamProvider.
uriStreamService.RegisterProvider(New ImageStreamProvider(xmlDataSet.Tables(0), "Photo"))

Add a Data Source

RichEditControl supports the following data source types:

Set the RichEditMailMergeOptions.DataSource property to specify the data source. If the data source contains multiple data tables, use the DataMember property to define a specific data member.

Note

The RichEditMailMergeOptions.DataSource property overrides the document-level Options.MailMerge.DataSource property.

Example: Use JSON as a Mail Merge Data Source

The following code snippet shows the NWindData model. The model defines a master-detail hierarchy (Customers → Orders → OrderDetails) used as the mail merge data source. It supplies nested collections so the merge engine can iterate through related records at each level.

csharp
public class NWindData {
    public List<Customer> Customers { get; set; } = new();

    public record Customer(
        string CustomerId,
        string CompanyName,
        string ContactName,
        string Country,
        string Address,
        string City,
        string Phone,
        List<Order> Orders);

    public record Order(
        string OrderID,
        DateTime? OrderDate,
        string ShipCountry,
        double Freight,
        List<OrderDetails> OrderDetails);

    public record OrderDetails(
        int ProductId,
        string ProductName,
        int Quantity,
        decimal UnitPrice,
        double Discount);
}
vb
Public Class NWindData
    Public Property Customers As List(Of Customer) = New List(Of Customer)()

    Public Class Customer
        Public Property CustomerId As String
        Public Property CompanyName As String
        Public Property ContactName As String
        Public Property Country As String
        Public Property Address As String
        Public Property City As String
        Public Property Phone As String
        Public Property Orders As List(Of [Order])

        Public Sub New(customerId As String, companyName As String, contactName As String, country As String, address As String, city As String, phone As String, orders As List(Of [Order]))
            Me.CustomerId = customerId
            Me.CompanyName = companyName
            Me.ContactName = contactName
            Me.Country = country
            Me.Address = address
            Me.City = city
            Me.Phone = phone
            Me.Orders = orders
        End Sub
    End Class

    Public Class [Order]
        Public Property OrderID As String
        Public Property OrderDate As DateTime?
        Public Property ShipCountry As String
        Public Property Freight As Double
        Public Property OrderDetails As List(Of OrderDetails)

        Public Sub New(orderID As String, orderDate As DateTime?, shipCountry As String, freight As Double, orderDetails As List(Of OrderDetails))
            Me.OrderID = orderID
            Me.OrderDate = orderDate
            Me.ShipCountry = shipCountry
            Me.Freight = freight
            Me.OrderDetails = orderDetails
        End Sub
    End Class

    Public Class OrderDetails
        Public Property ProductId As Integer
        Public Property ProductName As String
        Public Property Quantity As Integer
        Public Property UnitPrice As Decimal
        Public Property Discount As Double

        Public Sub New(productId As Integer, productName As String, quantity As Integer, unitPrice As Decimal, discount As Double)
            Me.ProductId = productId
            Me.ProductName = productName
            Me.Quantity = quantity
            Me.UnitPrice = unitPrice
            Me.Discount = discount
        End Sub
    End Class
End Class

The following code snippet sets up a data source. The System.Text.Json.JsonSerializer loads JSON data from a file as a list of NWindData objects. The deserialized NWindData object is then assigned to the DataSource property.

csharp
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
using System.Text.Json;
using word_processor_master_detail_merge;

// Build a path to the JSON data file relative to the project's base directory.
string dataPath = Path.Combine(
    AppDomain.CurrentDomain.BaseDirectory,
    "nwind_data.json"
);

// Extract the entire JSON file contents into a string.
string json = File.ReadAllText(dataPath);

// Declare a nullable NWind instance to hold deserialized data.
NWindData? nwind;
try
{
    // Deserialize the JSON payload to an NWindData instance.
    nwind = JsonSerializer.Deserialize<NWindData>(json);
}
catch (Exception ex)
{
    // Log the exception (including type and message) and abort further processing.
    Console.Error.WriteLine($"Deserialization failed: {ex}");
    return;
}

// Build a path to the DOCX template file relative to the project's base directory.
var templatePath = Path.Combine(
    AppDomain.CurrentDomain.BaseDirectory,
    "template.docx"
);

// Load the template into the word processor instance.
richEditControl.LoadDocument(templatePath);

// Create mail merge options.
MailMergeOptions myMergeOptions = richEditControl.Document.CreateMailMergeOptions();

// Assign the data source (Customers collection).
// Null-propagation prevents exceptions if deserialization has failed.
myMergeOptions.DataSource = nwind?.Customers;
vb
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native
Imports System.Text.Json
Imports word_processor_master_detail_merge
Imports System.IO

' Build a path to the JSON data file relative to the project's base directory.
Dim dataPath As String = Path.Combine( _
    AppDomain.CurrentDomain.BaseDirectory, _
    "nwind_data.json" _
)

' Extract the entire JSON file contents into a string.
Dim json As String = File.ReadAllText(dataPath)

' Declare a nullable NWind instance to hold deserialized data.
Dim nwind As NWindData = Nothing
Try
    ' Deserialize JSON to an NWind instance (System.Text.Json).
    ' If the JSON shape is invalid for NWind, a JsonException (or other exception) may be thrown.
    nwind = JsonSerializer.Deserialize(Of NWindData)(json)
Catch ex As Exception
    ' Log the exception (type + message) and abort processing.
    Console.Error.WriteLine($"Deserialization failed: {ex}")
    Return
End Try

' Build a path to the DOCX template file relative to the project's base directory.
Dim templatePath As String = Path.Combine( _
    AppDomain.CurrentDomain.BaseDirectory, _
    "template.docx" _
)

' Load the template into the word processor instance.
richEditControl.LoadDocument(templatePath)

' Create mail merge options.
Dim myMergeOptions As MailMergeOptions = richEditControl.Document.CreateMailMergeOptions()

' Assign data source (Customers collection).
' Null-propagation avoids exceptions if deserialization has failed.
myMergeOptions.DataSource = If(nwind?.Customers, Nothing)

Merge Data and Export the Result

Call the RichEditControl.MailMerge method to merge data into a template and save the document to the specified file or stream.

You can use the MailMergeOptions class properties to specify additional mail merge options. Pass the MailMergeOptions object as the MailMerge(MailMergeOptions, Document) method parameter to apply these options.

Example: Merge Records to a Single File

The following code snippet loads a template, specifies a data source, and exports merged records to a single file:

csharp
using DevExpress.Office.Services;
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
using System.Diagnostics;

// Build a path to the DOCX template file relative to the project's base directory.
var templatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "template.docx");

// Load the template into the word processor instance.
richEditControl.LoadDocument(templatePath);

// Create mail merge options.
MailMergeOptions myMergeOptions = richEditControl.Document.CreateMailMergeOptions();

// Assign the data source (Customers collection).
myMergeOptions.DataSource = nwind?.Customers;

// Each merged record starts in a new section, preserving page setup and headers/footers per record.
myMergeOptions.MergeMode = MergeMode.NewSection;

var outputPath = Path.Combine(Environment.CurrentDirectory, "result.docx");
richEditControl.MailMerge(myMergeOptions, outputPath, DocumentFormat.OpenXml);
vb
Imports DevExpress.Office.Services
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native
Imports System.Diagnostics
Imports System.IO

' Build a path to the DOCX template file relative to the project's base directory.
Dim templatePath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "template.docx")

' Load the template into the word processor instance.
richEditControl.LoadDocument(templatePath)

' Create mail merge options.
Dim myMergeOptions As MailMergeOptions = richEditControl.Document.CreateMailMergeOptions()
myMergeOptions.DataSource = xmlDataSet.Tables(0)

' Assign the data source (Customers collection). Null-propagation prevents exceptions if deserialization has failed.
myMergeOptions.DataSource = If(nwind?.Customers, Nothing)

' Each merged record starts in a new section, preserving page setup and headers/footers per record.
myMergeOptions.MergeMode = MergeMode.NewSection

Dim outputPath As String = Path.Combine(Environment.CurrentDirectory, "result.docx")
richEditControl.MailMerge(myMergeOptions, outputPath, DocumentFormat.OpenXml)

Example: Merge Records to Separate Files

Iterate through all records and use the MailMerge(MailMergeOptions, Document) method overload to merge each record to the temporary RichEditDocumentServer instance.

The following code snippet iterates through all records in the loop and exports each record to a separate DOCX file:

csharp
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;

// Build the absolute path to the mail merge template document (DOCX).
var templatePath = Path.Combine(
    AppDomain.CurrentDomain.BaseDirectory,
    "template.docx"
);

// Load the template into the word processor instance.
richEditControl.LoadDocument(templatePath);

// Create mail merge options.
MailMergeOptions myMergeOptions = richEditControl.Document.CreateMailMergeOptions();

// Assign the data source (Customers collection). Null-propagation prevents exceptions
// if deserialization has failed.
myMergeOptions.DataSource = nwind?.Customers;

// Declare a temporary RichEditDocumentServer.
var tempWordProcessor = new RichEditDocumentServer();

// Iterate through each customer record.
for (int i = 0; i <= nwind?.Customers.Count - 1; i++)
{
    // Restrict merge to a single record.
    myMergeOptions.FirstRecordIndex = i;
    myMergeOptions.LastRecordIndex = i;

    // Reset the temporary document before each merge.
    tempWordProcessor.CreateNewDocument();

    // Execute the mail merge for the current record.
    richEditControl.MailMerge(myMergeOptions, tempWordProcessor.Document);

    // Produce a unique output filename per record (result0.docx, result1.docx, etc.).
    var outputPath1 = Path.Combine(
        Environment.CurrentDirectory,
        string.Format("result{0}.docx", i)
    );

    // Save the merged document in Open XML (DOCX) format.
    tempWordProcessor.SaveDocument(outputPath1, DocumentFormat.OpenXml);
}
vb
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native
Imports System.IO

' Build the absolute path to the mail merge template document (DOCX).
Dim templatePath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "template.docx")

' Load the template into the word processor instance.
richEditControl.LoadDocument(templatePath)

' Create mail merge options.
Dim myMergeOptions As MailMergeOptions = richEditControl.Document.CreateMailMergeOptions()

' Assign the data source (Customers collection). Null-propagation prevents exceptions
' if deserialization has failed.
myMergeOptions.DataSource = If(nwind?.Customers, Nothing)

' Declare a temporary RichEditDocumentServer.
Dim tempWordProcessor As New RichEditDocumentServer()

' Iterate through each customer record.
For i As Integer = 0 To If(nwind?.Customers?.Count, -1)
    ' Restrict merge to a single record.
    myMergeOptions.FirstRecordIndex = i
    myMergeOptions.LastRecordIndex = i

    ' Reset the temporary document before each merge.
    tempWordProcessor.CreateNewDocument()

    ' Execute the mail merge for the current record.
    richEditControl.MailMerge(myMergeOptions, tempWordProcessor.Document)

    ' Produce a unique output filename per record (result0.docx, result1.docx, etc.).
    Dim outputPath1 As String = Path.Combine(Environment.CurrentDirectory, $"result{i}.docx")

    ' Save the merged document in Open XML (DOCX) format.
    tempWordProcessor.SaveDocument(outputPath1, DocumentFormat.OpenXml)
Next

Tip

If your template document contains fields that are not bound to database fields, you can replace those fields with their values (unlink fields). Use the following methods to do this:

Mail Merge Events

RichEditControl implements the following events so that you can control specific mail merge steps:

MailMergeStartedFires before mail merge starts.MailMergeFinishedFires when the mail merge is completed.MailMergeRecordStartedFires before each data record is merged with the document in the mail merge process.MailMergeRecordFinishedFires after each data record is merged with the document in the mail merge process.

The following example handles the MailMergeRecordStarted event. If the record’s CustomerID is ANTON , the record is skipped.

csharp
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;

richEditControl.MailMergeRecordStarted += (sender, e) => RichEditControl_OnMailMergeRecordStarted(sender, e, nwind?.Customers);

void RichEditControl_OnMailMergeRecordStarted(
    object sender,
    MailMergeRecordStartedEventArgs e,
    List<Customer> dataSet) {
    // Ensure the record index maps to an existing customer.
    if (e.RecordIndex >= 0 && e.RecordIndex < dataSet.Count)
    {
        // Get the current customer for this merge record.
        var current = dataSet[e.RecordIndex]; 

        // Skip merging this record if the CustomerId is ANTON (case-insensitive).
        if (string.Equals(current.CustomerId, "ANTON", StringComparison.OrdinalIgnoreCase))
        {
            e.Cancel = true;
        }
    }
}
vb
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native

AddHandler richEditControl.MailMergeRecordStarted, Sub(sender, e) RichEditControl_OnMailMergeRecordStarted(sender, e, If(nwind?.Customers, Nothing))

Private Sub RichEditControl_OnMailMergeRecordStarted(
    sender As Object,
    e As MailMergeRecordStartedEventArgs,
    dataSet As List(Of Customer))

    ' Ensure the record index maps to an existing customer.
    If e.RecordIndex >= 0 AndAlso e.RecordIndex < dataSet.Count Then
        ' Get the current customer for this merge record.
        Dim current = dataSet(e.RecordIndex)

        ' Skip merging this record if the CustomerId is ANTON (case-insensitive).
        If String.Equals(current.CustomerId, "ANTON", StringComparison.OrdinalIgnoreCase) Then
            e.Cancel = True
        End If
    End If
End Sub

Mail Merge Functionality in the User Interface

The RichEditControl ships with the Mail Merge ribbon tab which allows users to insert merge fields, toggle field codes and results and view the merged data. Refer to the Create a Simple Rich Text Editor topic for details on how to create a ribbon UI for the RichEditControl.

See Also

Fields in WPF Rich Text Editor