Use Office File API to Implement Document (DOCX) Merge Operations
- 6 minutes to read
This tutorial explains how to use Word Processing Document API (RichEditDocumentServer) to implement document merge operations. In this tutorial, you create an app where you incorporate edit form data into a templated .docx document.
Create a .NET MAUI App
Follow the instructions in the following section to create an app with our .NET MAUI components: Get Started.
Note that the DevExpress library for .NET MAUI targets only Android and iOS platforms. See also: Supported Platforms.
Add Office File API NuGet Packages
Install the following NuGet packages to use Word Processing API in your app:
See also: Use Office File API in .NET MAUI Applications (macOS, iOS, Android).
Copy Files to App Data Directory
Add a templated docx file (“Party Invitation.docx” in this example) to the Resources/Raw folder. Then, copy this file from the application bundle to the app’s data folder to access it from code.
Note: The templated Party Invitation.docx file is available in our Merge Editor Data to a Templated Document example repo on GitHub: Party Invitation.docx.
private async void OnLoaded(object sender, EventArgs e) {
await InitFilesAsync("Party Invitation.docx");
}
async Task InitFilesAsync(string fileName) {
using Stream fileStream = await FileSystem.Current.OpenAppPackageFileAsync(fileName);
string targetFile = Path.Combine(FileSystem.Current.AppDataDirectory, fileName);
using FileStream outputStream = File.OpenWrite(targetFile);
fileStream.CopyTo(outputStream);
}
For more information, refer to the following help page: File system helpers.
Implement View Model
This tutorial uses MVVM Toolkit features to build a View Model. To use this library in your app, install the CommunityToolkit.Mvvm
NuGet package.
The list below includes the main MVVM toolkit features used in this tutorial:
- ObservableObject class - Implements INotifyPropertyChanged and INotifyPropertyChanging interfaces that their descendants use for property change notifications.
- ObservableProperty attribute - Allows you to generate observable properties from annotated fields.
- RelayCommand attribute - Allows you to generate relay command properties for annotated methods.
Add a MergeInfoViewModel class with the publicName
field and MergeAsync
method as follows:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
//...
public partial class MergeInfoViewModel : ObservableObject {
[ObservableProperty]
string publicName = "Alice";
[RelayCommand]
async Task MergeAsync() {
RichEditDocumentServer mergeProcessor = await RichProcFromFileAsync("Party Invitation.docx");
AssignSource(mergeProcessor);
mergeProcessor = MergeToNewDocument(mergeProcessor);
string docPath = await SaveToFile(mergeProcessor, "ResultingDoc.docx");
await ShareDocAsync(docPath);
}
// See the sections below for implementation details of the above methods.
}
Create a Text Processor and Load a Document Template
The RichEditDocumentServer allows you to perform operations with text, such as merge, export to different formats, and so on.
Create a RichEditDocumentServer instance and call the LoadDocumentAsync method to load the templated .docx file for further processing.
async Task MergeAsync() {
RichEditDocumentServer mergeProcessor = await RichProcFromFileAsync("Party Invitation.docx");
//...
}
async Task<RichEditDocumentServer> RichProcFromFileAsync(string fileName) {
string workingFilePath = Path.Combine(FileSystem.Current.AppDataDirectory, fileName);
RichEditDocumentServer mergeRichProcessor = new RichEditDocumentServer();
await mergeRichProcessor.LoadDocumentAsync(workingFilePath);
return mergeRichProcessor;
}
Specify Data to Merge
Add an AssignSource
method that defines how to fill in templated document fields. The first field (date-time) is locked to prevent changes. Assign an object with data to be incorporated into the template to the DataSource. The object’s property names should match field names in the templated document.
async Task MergeAsync() {
//...
AssignSource(mergeProcessor);
//...
}
void AssignSource(RichEditDocumentServer mergeProcessor) {
mergeProcessor.Document.Fields[0].Locked = true;
mergeProcessor.Options.MailMerge.DataSource = new List<object> { new {
RecipientName = this.PublicName,
SenderCompany = "DX_Company" } };
}
Merge Data
To merge data to the templated document, call the MailMerge method.
async Task MergeAsync() {
//...
mergeProcessor = MergeToNewDocument(mergeProcessor);
//...
}
RichEditDocumentServer MergeToNewDocument(RichEditDocumentServer existingDoc) {
RichEditDocumentServer resultDocumentProcessor = new RichEditDocumentServer();
resultDocumentProcessor.CreateNewDocument();
MailMergeOptions myMergeOptions = existingDoc.Document.CreateMailMergeOptions();
myMergeOptions.MergeMode = MergeMode.NewSection;
existingDoc.MailMerge(resultDocumentProcessor.Document);
return resultDocumentProcessor;
}
For more information, refer to the following help topic: Mail Merge in Word Processing Document API.
Save and Share the Resulting Document
Call the SaveDocumentAsync method to save the resulting merged document to a separate file with a specified path and format.
To implement the Share functionality in the UI, use the standard IShare
interface available through the Share.Default property. For more information, refer to the following page: Share.
async Task MergeAsync() {
//...
string docPath = await SaveToFile(mergeProcessor, "ResultingDoc.docx");
await ShareDocAsync(docPath);
}
async Task<string> SaveToFile(RichEditDocumentServer mergeProcessor, string fileName) {
string fileToSavePath = Path.Combine(FileSystem.Current.AppDataDirectory, fileName);
await mergeProcessor.SaveDocumentAsync(fileToSavePath, DocumentFormat.OpenXml);
return fileToSavePath;
}
async Task ShareDocAsync(string docPath) {
await Share.Default.RequestAsync(new ShareFileRequest {
Title = "Share the file",
File = new ShareFile(docPath)
});
}
Create an Input Form
In this step, create an input form with a TextEdit bound to the corresponding merged property to dynamically change its value.
- Initialize the
ContentPage.BindingContext
property with aMergeInfoViewModel
instance. - Add a TextEdit to implement an edit box. Bind its
Text
to thePublicName
property (this property is automatically generated because the ViewModel’spublicName
field is labeled with theObservableProperty
attribute). - Add a DXButton and bind its
Command
to theMergeCommand
property (this property is automatically generated because the ViewModel’sMergeAsync()
method is labeled with theRelayCommand
attribute).
<ContentPage ...
xmlns:dx="http://schemas.devexpress.com/maui"
Loaded="OnLoaded">
<ContentPage.BindingContext>
<local:MergeInfoViewModel/>
</ContentPage.BindingContext>
<dx:DXStackLayout ItemSpacing="20"
Padding="20">
<dx:TextEdit Text="{Binding PublicName}"
PlaceholderText="Input recipient name"
LabelText="Name"/>
<dx:DXButton Command="{Binding MergeCommand}"
Content="Merge" />
</dx:DXStackLayout>
</ContentPage>
Results
The following code snippets contain the complete code mentioned in the previous sections:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:dx="http://schemas.devexpress.com/maui"
xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
xmlns:local="clr-namespace:MauiOFAMerge"
x:Class="MauiOFAMerge.MainPage"
Loaded="OnLoaded">
<ContentPage.BindingContext>
<local:MergeInfoViewModel/>
</ContentPage.BindingContext>
<dx:DXStackLayout ItemSpacing="20"
Padding="20">
<dx:TextEdit Text="{Binding PublicName}"
PlaceholderText="Input recipient name"
LabelText="Name"/>
<dx:DXButton Command="{Binding MergeCommand}"
Content="Merge" />
</dx:DXStackLayout>
</ContentPage>
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
namespace MauiOFAMerge {
public partial class MainPage : ContentPage {
public MainPage() {
InitializeComponent();
}
private async void OnLoaded(object sender, EventArgs e) {
await InitFilesAsync("Party Invitation.docx");
}
async Task InitFilesAsync(string fileName) {
using Stream fileStream = await FileSystem.Current.OpenAppPackageFileAsync(fileName);
string targetFile = Path.Combine(FileSystem.Current.AppDataDirectory, fileName);
using FileStream outputStream = File.OpenWrite(targetFile);
fileStream.CopyTo(outputStream);
}
}
public partial class MergeInfoViewModel : ObservableObject {
[ObservableProperty]
string publicName = "Alice";
[RelayCommand]
async Task MergeAsync() {
RichEditDocumentServer mergeProcessor = await RichProcFromFileAsync("Party Invitation.docx");
AssignSource(mergeProcessor);
mergeProcessor = MergeToNewDocument(mergeProcessor);
string docPath = await SaveToFile(mergeProcessor, "ResultingDoc.docx");
await ShareDocAsync(docPath);
}
async Task<RichEditDocumentServer> RichProcFromFileAsync(string fileName) {
string workingFilePath = Path.Combine(FileSystem.Current.AppDataDirectory, fileName);
RichEditDocumentServer mergeRichProcessor = new RichEditDocumentServer();
await mergeRichProcessor.LoadDocumentAsync(workingFilePath);
return mergeRichProcessor;
}
void AssignSource(RichEditDocumentServer mergeProcessor) {
mergeProcessor.Document.Fields[0].Locked = true;
mergeProcessor.Options.MailMerge.DataSource = new List<object> { new {
RecipientName = this.PublicName,
SenderCompany = "DX_Company" } };
}
RichEditDocumentServer MergeToNewDocument(RichEditDocumentServer existingDoc) {
RichEditDocumentServer resultDocumentProcessor = new RichEditDocumentServer();
resultDocumentProcessor.CreateNewDocument();
MailMergeOptions myMergeOptions = existingDoc.Document.CreateMailMergeOptions();
myMergeOptions.MergeMode = MergeMode.NewSection;
existingDoc.MailMerge(resultDocumentProcessor.Document);
return resultDocumentProcessor;
}
async Task<string> SaveToFile(RichEditDocumentServer mergeProcessor, string fileName) {
string fileToSavePath = Path.Combine(FileSystem.Current.AppDataDirectory, fileName);
await mergeProcessor.SaveDocumentAsync(fileToSavePath, DocumentFormat.OpenXml);
return fileToSavePath;
}
async Task ShareDocAsync(string docPath) {
await Share.Default.RequestAsync(new ShareFileRequest {
Title = "Share the file",
File = new ShareFile(docPath)
});
}
}
}