Back to Devexpress

Word Processing Document API: Use Mail Merge to Transform Plain or Master-Detail Data into Personalized Documents or Reports

officefileapi-15277-word-processing-document-api-mail-merge.md

latest42.1 KB
Original Source

Word Processing Document API: Use Mail Merge to Transform Plain or Master-Detail Data into Personalized Documents or Reports

  • Nov 03, 2025
  • 20 minutes to read

This topic describes the mail merge functionality available in the Word Processing Document API. 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, RichEditDocumentServer replaces fields with actual data from your data source.

Call the RichEditDocumentServer.LoadDocument or RichEditDocumentServer.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 template document:

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

using var wordProcessor = new RichEditDocumentServer();

var templatePath = Path.Combine(
    AppDomain.CurrentDomain.BaseDirectory,
    "template.docx"
);

// Load the template into the word processor instance.
wordProcessor.LoadDocument(templatePath);
//...
vb
Imports DevExpress.Office.Services
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native

Using wordProcessor As New RichEditDocumentServer()

    ' 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.
    wordProcessor.LoadDocument(templatePath)
End Using

Define Master-Detail Regions in a Template

Word Processing Document API 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:

Tip

If your template uses start and end tags other than TableStart and TableEnd, specify them with the MailMergeOptions.RegionStartTag and MailMergeOptions.RegionEndTag properties.

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;

// Create a `RichEditDocumentServer` instance
using var wordProcessor = new RichEditDocumentServer();

// 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.
wordProcessor.LoadDocument(templatePath);

var regionInfo = wordProcessor.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

' Create a `RichEditDocumentServer` instance
Using wordProcessor As New RichEditDocumentServer()

    ' 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.
    wordProcessor.LoadDocument(templatePath)

    Dim regionInfo = wordProcessor.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()
End Using

Consider the following rules when organizing master-detail regions. RichEditDocumentServer 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;
//...

// Create a RichEditDocumentServer instance
using var wordProcessor = new RichEditDocumentServer();

// 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"
);

wordProcessor.LoadDocument(templatePath);
GenerateMergeFields(wordProcessor.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
'...

' Create a RichEditDocumentServer instance
Using wordProcessor As New RichEditDocumentServer()

    ' 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" _
    )

    wordProcessor.LoadDocument(templatePath)
    GenerateMergeFields(wordProcessor.Document)
End Using

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 RichEditDocumentServer.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 RichEditDocumentServer.CalculateDocumentVariable event to insert an image from a DataSet.

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

using var wordProcessor = new RichEditDocumentServer();

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

static void WordProcessor_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

Using wordProcessor As New RichEditDocumentServer()

' Subscribe to the CalculateDocumentVariable event
AddHandler wordProcessor.CalculateDocumentVariable, Sub(sender, e) 
WordProcessor_CalculateDocumentVariable(sender, e, dataSet)

Private Shared Sub WordProcessor_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 following code snippet shows the IUriStreamProvider implementation used to insert images from a database. The INCLUDEPICTURE field in the template has a nested MERGEFIELD. This field refers to the EmployeeID field from the database. The 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;

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

' Create and dispose the RichEditDocumentServer automatically.
Using wordProcessor As New RichEditDocumentServer()
    ' ...
    ' Obtain the IUriStreamService from the RichEditDocumentServer.
    Dim uriStreamService As IUriStreamService = wordProcessor.GetService(Of IUriStreamService)()

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

    ' ...
End Using

Add a Data Source

RichEditDocumentServer 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;
}

// Create a RichEditDocumentServer instance.
using var wordProcessor = new RichEditDocumentServer();

// 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.
wordProcessor.LoadDocument(templatePath);

// Create mail merge options.
MailMergeOptions myMergeOptions = wordProcessor.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

' Create a RichEditDocumentServer instance.
Using wordProcessor As New RichEditDocumentServer()

    ' 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.
    wordProcessor.LoadDocument(templatePath)

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

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

Merge Data and Export the Result

Call the Document.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.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
using System.Diagnostics;

// Create a RichEditDocumentServer instance.
using var wordProcessor = new RichEditDocumentServer();

// 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.
wordProcessor.LoadDocument(templatePath);

// Create mail merge options.
MailMergeOptions myMergeOptions = wordProcessor.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");
wordProcessor.MailMerge(myMergeOptions, outputPath, DocumentFormat.OpenXml);

Process.Start(new ProcessStartInfo(outputPath) { UseShellExecute = true });
vb
Imports DevExpress.XtraRichEdit
Imports DevExpress.XtraRichEdit.API.Native
Imports System.Diagnostics
Imports System.IO

' Create a RichEditDocumentServer instance.
Using wordProcessor As New RichEditDocumentServer()

    ' 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.
    wordProcessor.LoadDocument(templatePath)

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

    ' 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")
    wordProcessor.MailMerge(myMergeOptions, outputPath, DocumentFormat.OpenXml)

    Process.Start(New ProcessStartInfo(outputPath) With {.UseShellExecute = True})
End Using

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;

// Create a RichEditDocumentServer instance
using var wordProcessor = new RichEditDocumentServer();

// 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.
wordProcessor.LoadDocument(templatePath);

// Create mail merge options.
MailMergeOptions myMergeOptions = wordProcessor.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.
    wordProcessor.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

' Create a RichEditDocumentServer instance
Using wordProcessor As New RichEditDocumentServer()

    ' 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.
    wordProcessor.LoadDocument(templatePath)

    ' Create mail merge options.
    Dim myMergeOptions As MailMergeOptions = wordProcessor.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.
        wordProcessor.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
End Using

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

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

MailMergeStartedFires before mail merge starts.MailMergeFinishedFires when 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;

using var wordProcessor = new RichEditDocumentServer();

wordProcessor.MailMergeRecordStarted += (sender, e) => WordProcessor_OnMailMergeRecordStarted(sender, e, nwind?.Customers);

void WordProcessor_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

Using wordProcessor As New RichEditDocumentServer()

AddHandler wordProcessor.MailMergeRecordStarted, Sub(sender, e) WordProcessor_OnMailMergeRecordStarted(sender, e, If(nwind?.Customers, Nothing))

Private Sub WordProcessor_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

Complete Examples

Refer to the GitHub examples below for complete code snippets:

See the following GitHub examples for more mail merge scenarios:

Use Reporting Components to Create Mail-Merge Documents

One more way to create mail-merge documents with greater flexibility is to use Reporting Components : they support multiple data nesting levels, different data sources, and data formatting. Refer to the following help topic for details: Reporting - Use Embedded Fields (Mail Merge).

To use Reporting Components, you must own a license to an appropriate DevExpress subscription: Compare Subscriptions.

Note that you can download a fully-functional 30-day trial version and try this functionality in your project:

Download: Free 30-Day Trial