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
- Stage 1 — Get started and load data / GitHub sample
- Stage 2 — Localize UI captions / GitHub sample
- Stage 3 — Sort and filter data / GitHub sample
- Stage 4 — Edit and validate data / GitHub sample
- Stage 5 — Authenticate users and protect data / GitHub sample
- Stage 6 — Preview and download reports / GitHub sample
- Stage 7 — Mail Merge or download Office documents / GitHub sample (this post)
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.
Enable the Office features at the Module level
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.
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.
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.