Skip to main content

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.

DevExpress Office File API in .NET MAUI app - Merge functionality

View Example

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:

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 a MergeInfoViewModel instance.
  • Add a TextEdit to implement an edit box. Bind its Text to the PublicName property (this property is automatically generated because the ViewModel’s publicName field is labeled with the ObservableProperty attribute).
  • Add a DXButton and bind its Command to the MergeCommand property (this property is automatically generated because the ViewModel’s MergeAsync() method is labeled with the RelayCommand 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:

View Example

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

Send Template Messages with Mail Merge

Mail Merge Featured Scenario