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.

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:
-
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;
}
}
...
-
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);
...
-
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);
...
-
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!
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.