officefileapi-15277-word-processing-document-api-mail-merge.md
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:
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:
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);
//...
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
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:
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();
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 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:
| Field | Description | Supports Formatted Content | Supports Image Insertion |
|---|---|---|---|
| MERGEFIELD | Inserts plain text from a data source column. | No | No |
| DOCVARIABLE | Inserts dynamically generated or formatted content supplied in the CalculateDocumentVariable event. | Yes (paragraphs, formatted runs, tables) | Yes |
| INCLUDEPICTURE | Inserts an image from a file path, database, or external source (HTML/MHT with URIs). | N/A | Yes |
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.
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");
}
//...
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
'...
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:
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.
Handle the RichEditDocumentServer.CalculateDocumentVariable event to insert an image from a DataSet.
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;
}
}
}
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
Use the IUriStreamProvider implementation to ensure that you load images and not just references to these images. This applies to images loaded from:
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.
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;
}
}
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.
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"));
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
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.
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.
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);
}
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.
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;
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
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.
The following code snippet loads a template, specifies a data source, and exports merged records to a single file:
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 });
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
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:
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);
}
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:
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.
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;
}
}
}
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
Refer to the GitHub examples below for complete code snippets:
See the following GitHub examples for more mail merge scenarios:
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: