JavaScript — Consume the DevExpress Backend Web API with Svelte (Part 7. Mail Merge)

News
22 December 2023

In the first part of this post series, I described how to set up a Svelte Kit project to load data from the DevExpress Web API service. The second part described how I queried metadata from the service, in order to display captions customized in the Application Model. Part 3 covered sorting and filtering of data, as examples of data retrieval customized dynamically by the user at runtime. In Part 4 I added editing functionality to the demo app, including validation. Part 5 added user authentication and authorization functionality to the demo app. Report came in for Part 6.

Table of Contents

You can find source code for each stage of the demo in the GitHub repository. Note: once more, I updated DevExpress library versions for the Stage 7 branch, so the projects now use version 23.2.3. Since the step to 23.2 pre-release versions had already been made, no other changes were necessary in this regard.

Additionally, I also updated the implementations of the metadata and the schema services. These take advantage of the JSON capable endpoint now and don’t require an XML library.

The Mail Merge feature I want to focus on in this post relies on the DevExpress Office File API. We recently published a blog post and sample to illustrate how this powerful cross-platform API can be used in a stand-alone Web API application. XAF has support for this functionality, and we decided to add it to the XAF Web API demo project as well.

Important Note: The Office File API functionality described in this post requires either the DevExpress Office File API Subscription or DevExpress Universal Subscription with our free Web API Service. XAF Blazor UI and its Office module requires the DevExpress Universal Subscription.

Enable the Office features at the Module level

As before, I begin at the bottom of the architecture stack, by adding the extra package for the XAF Office Module to the XAFApp.Module project. In XAFApp/XAFApp.Module/XAFApp.Module.csproj I have this now:

<ItemGroup>
  ...
  <PackageReference Include="DevExpress.ExpressApp.ReportsV2" Version="23.2.3"/>
  <PackageReference Include="DevExpress.ExpressApp.Office" Version="23.2.3"/>
  <PackageReference Include="DevExpress.Persistent.Base" Version="23.2.3"/>
  ...
</ItemGroup>

I also modify XAFApp/XAFApp.Module/Module.cs to add the module type in the constructor:

public XAFAppModule() {
  AdditionalExportedTypes.Add(typeof(ApplicationUser));
  ...
  RequiredModuleTypes.Add(typeof(ReportsModuleV2));
  RequiredModuleTypes.Add(typeof(OfficeModule));

  SecurityModule.UsedExportedTypes = UsedExportedTypes.Custom;
}

Now we come to the most impressive part of this implementation: the type RichTextMailMergeData. This type is documented in the XAF docs, for both EF and XPO. On its basis, the built-in UI modules for XAF applications supply visual designers for mail merging templates.

Note that RichTextMailMergeData provides some complex functionality out of the box. It allows the persistence of a mail merge template in Rich Text Format to any supported database, and it is automatically protected by security mechanisms activated in the Blazor App and the Web API Service in the demo solution. These mechanisms restrict not only access to instances of the type itself, but also to the target data types that can be used in mail merge operations. This is a very powerful feature, and it is available with very little effort.

I add a line to the Entity Framework context in XAFApp/XAFApp.Module/BusinessObjects/XAFAppDbContext.cs, to make the type RichTextMailMergeData available for persistence.

Enable the Office features on the Blazor level

Taking advantage of the special data type RichTextMailMergeData, the Blazor module can make its template designer available if it is initialized correctly. I add the required package reference to XAFApp/XAFApp.Blazor.Server/XAFApp.Blazor.Server.csproj:

...
<PackageReference Include="DevExpress.ExpressApp.ReportsV2.Blazor" Version="23.2.3"/>
<PackageReference Include="DevExpress.ExpressApp.Office.Blazor" Version="23.2.3"/>
<PackageReference Include="DevExpress.ExpressApp.Validation.Blazor" Version="23.2.3"/>
...

Then I add a call to AddOffice to the builder chain in XAFApp/XAFApp.Blazor.Server/Startup.cs:

services.AddXaf(Configuration, builder => {
  builder.UseApplication<XAFAppBlazorApplication>();
  builder.Modules
    .AddReports(options => { ... })
    .AddOffice(options => {
      options.RichTextMailMergeDataType = typeof(RichTextMailMergeData);
    })
    .Add<XAFAppModule>()
...

The data type is configured in this block, you can use your own implementation instead of the standard one if you prefer.

With these changes in place, I run the Blazor app and create a test template associated with the SaleProduct demo data type. The controls on the Mail Merge tab of the designer are available to set up the template. Please follow this link for more detailed information about the mail merge feature.

Once the template has been saved and the app reloaded, I can use the button Show in document with a few selected rows to display a preview of the merged document for the SaleProduct type.

Add Mail Merge to the Web API Service

Two steps are required to allow the Web API Service of the demo project to execute mail merging. First, the type RichTextMailMergeData needs to be exposed so that service users can query it. The actual merge feature will use the ID of a persisted template, and listing existing instances is therefore a requirement.

I add the type to the list passed to the builder in XAFApp/XAFApp.WebApi/Startup.cs. This adds a CRUD endpoint for the type, as described in the documentation.

services.AddXafWebApi(builder => {
  builder.ConfigureOptions(options => {
    options.BusinessObject<SaleProduct>();
    options.BusinessObject<ReportDataV2>();
    options.BusinessObject<RichTextMailMergeData>();
  });
...

The second step is the more complicated one. A new controller is required to provide an entry point for the desired mail merging process(es). In the demo I add just one entry point with some flexibility, but in a real application you may require extra parameters or more entry points for different workflows.

You can find the complete source code of the controller at this link. The implemented logic has 4 steps:

  1. The controller receives the injected reference to the IObjectSpaceFactory. This method is described in detail on this documentation page.
[Authorize]
[Route("api/[controller]")]
public class MailMergeController : ControllerBase, IDisposable {
  private readonly IObjectSpaceFactory objectSpaceFactory;

  public MailMergeController(IObjectSpaceFactory objectSpaceFactory) {
    this.objectSpaceFactory = objectSpaceFactory;
  }

  private IObjectSpace objectSpace;

  public void Dispose() {
    if (objectSpace != null) {
      objectSpace.Dispose();
      objectSpace = null;
    }
  }
...
  1. Using the ID of the mail merge data instance and an optional list of target object IDs, the controller method MergeDocument loads all relevant objects from the data layer.
...
  [HttpGet("MergeDocument({mailMergeId})/{objectIds?}")]
  public async Task<object> MergeDocument(
    [FromRoute] string mailMergeId,
    [FromRoute] string? objectIds) {
    // Fetch the mail merge data by the given ID
    objectSpace = objectSpaceFactory.CreateObjectSpace<RichTextMailMergeData>();
    RichTextMailMergeData mailMergeData =
      objectSpace.GetObjectByKey<RichTextMailMergeData>(new Guid(mailMergeId));

    // Fetch the list of objects by their IDs
    List<Guid> ids = objectIds?.Split(',').Select(s => new Guid(s)).ToList();
    IList dataObjects = ids != null
      ? objectSpace.GetObjects(mailMergeData.DataType, new InOperator("ID", ids))
      : objectSpace.GetObjects(mailMergeData.DataType);
...
  1. The target object data source and the requested template are connected to a RichEditDocumentServer instance. After the merge operation completes, a secondary instance is used to export to PDF.
...
    using RichEditDocumentServer server = new();
    server.Options.MailMerge.DataSource = dataObjects;
    server.Options.MailMerge.ViewMergedData = true;
    server.OpenXmlBytes = mailMergeData.Template;

    MailMergeOptions mergeOptions = server.Document.CreateMailMergeOptions();
    mergeOptions.MergeMode = MergeMode.NewSection;

    using RichEditDocumentServer exporter = new();
    server.Document.MailMerge(mergeOptions, exporter.Document);

    MemoryStream output = new();
    exporter.ExportToPdf(output);
...
  1. Finally, the PDF data is returned to the caller using the same approach I demonstrated previously for Reports.
...
    output.Seek(0, SeekOrigin.Begin);
    return File(output, MediaTypeNames.Application.Pdf);
  }

Add the Mail Merge feature to the Svelte Kit JavaScript frontend application

Since the output from the Mail Merge feature is a PDF document, the client-side support functionality is similar to the Report previewer. A table overview of RichEditMailMergeData objects is generated by the new page files in svelte-frontend/src/routes/mailmerge/+page.server.js and .../+page.svelte. The new row action button in svelte-frontend/src/lib/MailMergeRowActionButtons.svelte directs to a new preview component.

The preview page was cloned from that used by the Report preview. The two could be refactored easily to use a common component, but I decided to leave them separate so that individual features of the demo should be easier to follow and distinguish. The two required files are in the path svelte-frontend/src/routes/viewMailMergeDocument/[key]/[[targetIds]], which uses Svelte Kit file system routing to establish the two parameters key and targetIds (the latter is optional, hence the double brackets). For your reference, here you can find +page.server.js and +page.svelte.

The previewer is activated using the key parameter when the action button for the mail merge item is clicked. Since targetIds is not used in that scenario, all target objects are included in the merged document.

The implementation of the Mail Merge feature can take a list of target IDs into account and limit the rows accordingly. For the demo, I have added an extension to the data table of Sale Products, so that the merge document can be displayed for any individual row. I did not want to complicate the changes at this point to incorporate multi-selection, but technically this would only be a small extension of the functionality.

The most important addition occurred in svelte-frontend/src/routes/saleProducts/+page.server.js. Previously, the load function simply called loadData with the details for the SaleProduct data type. Now I added a piece of code that retrieves all those instances of RichTextMailMergeData which refer to SaleProduct by way of their TargetTypeFullName properties. For the demo, I only use the first such object I find, passing its ID onwards to the page rendering process. Of course this could be extended to pass on all relevant IDs and display a menu for the user to choose from.

const qbParams = {
	filter: {
 		TargetTypeFullName: 'XAFApp.Module.BusinessObjects.SaleProduct'
	}
};
const mailMergeDocumentId = await fetch(
	`http://webapi:5273/api/odata/RichTextMailMergeData${queryBuilder(qbParams)}`,
	{ redirect: 'manual' }
)
	.then((res) => {
		if (res.ok) return res;
		throw new Error(`HTTP error, status: ${res.status}`);
	})
	.then((res) => res.json())
	.then((res) => res.value && res.value.length > 0 && res.value[0].ID);

The newly retrieved ID of a mail merge data item is used in +page.svelte to configure the row action button. In svelte-frontend/src/lib/ShowRowDocumentActionButtons.svelte you can see the URL constructed to pass both the mail merge item ID and the target ID in the href attribute.

<a
  class="border-2 rounded bg-white px-2 py-0.5 hover:bg-orange-200"
  href="/viewMailMergeDocument/{mailMergeDocumentId}/{row.ID}"
  alt="View"><span class="fa fa-file-pdf-o" /></a
>

Using the new row action button, the user can now display a merge document for just one Sale Product. You can see the two-part URL in the screenshot.

Conclusion

It is particularly interesting to see how easily functionality provided by the XAF infrastructure can be surfaced in the Web API Service, and attached to from the JavaScript app. Depending on requirements, more such bindings may be included in the box, so please let us know your thoughts!

Here is the usual link to the GitHub branch for this post: “stage-7”.

Thanks for reading!

For related information, please review the following articles: XAF Blazor | Getting Started Tutorials | Common Questions about the New DevExpress Web API Service. You can get your free copy of .NET App Security Library & Web API Service here: https://www.devexpress.com/security-api-free. To learn about the advanced/paid features of our Web API Service, please refer to the following help topic: Obtain a Report from a Web API Controller Endpoint.

Your Feedback Matters!

Please take a moment to reply to the following questions – your feedback will help us shape/define future development strategies.

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.