JavaScript — Consume the DevExpress Backend Web API with Svelte (Part 2. Localize UI Captions)

News
17 April 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. I started at the beginning, and the sample ended with a very simple data overview — please check out the branch “stage-1” in the GitHub repository for the code from the first post.

For this second part, I decided to improve my simple data visualization by making an important step: I want to integrate information from the type and setting metadata behind the Web API service for dynamic UI localization of the JavaScript client. Here is the branch “stage-2” in the GitHub repository, which includes the changes described in this post. Side note: we have recently published a sample along similar lines for .NET MAUI. Please follow this link to read the blog post.

Table of Contents

A built-in storage for application-wide type, behavior and UI settings

The Application Model is well known to XAF developers; it’s a data structure which is auto-generated by extracting metadata from persistent types and other sources, which can then be overridden selectively by developers. The framework identifies business model classes and several other supported types, analyzes them and builds metadata. That metadata is the Application Model, and it can be customized by applying Model Diffs. For example, the Module project in the sample repository includes a file called Model.DesignedDiffs.xafml. Below you’ll find an example where this file is edited manually, but in Visual Studio you can also bring up the visual Model Editor by double-clicking this file. It is also possible to store XAFML settings in a database, which is the default used for XAF Blazor and WinForms UI apps since it allows application admins to manage settings for different user roles at runtime.

Illustration of the Model Editor in Visual Studio

For this demo, the most obviously relevant information relates to the data you see on screen. There are field names displayed in table column headers, and the type name of the data (i.e. SaleProduct) is also used in the UI. The Application Model already includes metadata about known types and their properties, and even in simple cases it is useful to retrieve details like field captions from that store because they are “prettified” by applying capitalization and adding spaces. The Application Model allows translation of field and type names, as well as any custom identifiers you may need (you can define custom localization items under the Localization node in the Model Editor).

The localization and Application Model editing functionality or the Web API Service ships as part of the DevExpress Universal Subscription.

Since the sample data type SaleProduct only has two properties called Name and Price, I begin by introducing a “Model Diff” that customizes these names for test purposes. If you’re working along, edit the file Model.DesignedDiffs.xafml in the Module project and change it to be similar to this:

<?xml version="1.0"?>
<Application Title="XAFApp">
  <BOModel>
    <Class Name="XAFApp.Module.BusinessObjects.SaleProduct">
      <OwnMembers>
        <Member Name="Name" Caption="Product Name" />
        <Member Name="Price" Caption="Product Price" />
      </OwnMembers>
    </Class>
  </BOModel>
</Application>

Both fields are assigned custom captions in this small snippet. To test that this change is recognized by the Web API service, you can use the Swagger UI. With your service running, access http://localhost:5273/swagger in the browser and find the entry for /api/Localization/ClassCaption. Click the button Try it out and enter the name of the class in the field classFullName: XAFApp.Module.BusinessObjects.SaleProduct. Click Execute and you’ll see the result: “Sale Product”. The space has been inserted by the service — it works!

Result of calling MemberCaption in the Swagger UI

Now use the endpoint /api/Localization/MemberCaption, repeat the class name as before and try Name and Price for the memberName. You will receive the customized captions “Product Name” and “Product Price”, which you configured in the Model Diffs.

Of course you can use a tool like curl to run queries like these (the Swagger UI shows example commands for curl). Other tools are popular to test APIs, including Postman and RapidAPI.

Illustration of calling MemberCaption in RapidAPI

Note that the service endpoints seem to support the Accept-Language header to pass the localization language. In tests I found that this does not work correctly in some environments, including the Docker/Linux based runtime I used for the demo project. However, this issue will of course be fixed, and then you will be able to take advantage of Application Model-based localization features and retrieve localized strings correctly through the Web API service endpoints.

Automate retrieval of schema information per data type

In order to display several fields in the UI of the Svelte app, like the overview page does, it is necessary at this time to make numerous requests to the Web API. This is a situation that may see improvements in the future, but in the meantime we need to make one request per field, plus one for the type itself, and perhaps additional ones to retrieve other bits and pieces of information. Additionally, we need to retrieve structural metadata before we can call the various field- and type-specific endpoints!

It may be necessary, as you’ll see going forward, to have some field-specific configuration in the Svelte app anyway, but generally it is a better approach not to reproduce type information in the client application. The Web API service already knows everything there is to know about the data structures it works with, so it’s better to ask the service instead of setting up a second static copy of these details.

In the Swagger UI you can see the service endpoint /api/odata/$metadata. Try it, it does not require any parameters. It returns a long blob of XML data, which includes a Schema block about the SaleProduct type and its members.

There are a couple of issues with this API endpoint. First, it can’t be used to return data selectively, so it is potentially inefficient to use. Second, while the Swagger UI is pre-configured to ask for data of type application/json, the service clearly returns XML data instead. Again, these are problems which will be addressed in the future, but for now a bit of work is required to use the service endpoint as intended.

To work along, please add the package fast-xml-parser to your project:

> pnpm i -D fast-xml-parser

Now create the file src/routes/api/metadata/+server.js in the Svelte Kit project folder. Here is the content:

import { json } from '@sveltejs/kit';
import { XMLParser } from 'fast-xml-parser';

// Could prerender to prevent extra roundtrips to the XML data
// export const prerender = true;

const parser = new XMLParser({
	ignoreAttributes: false,
	attributeNamePrefix: '',
	attributesGroupName: '@_attributes'
});

export async function GET({ fetch }) {
	const result = await fetch('http://webapi:5273/api/odata/$metadata')
		.then((res) => res.text())
		.then((xmlString) => parser.parse(xmlString));

	return json(result);
}

This service does one simple job: it retrieves the XML format metadata from the Web API service and converts it to a JSON representation. Since the conversion is automatic, this is not the best possible JSON structure you can imagine, but it’s much easier to work with in JavaScript than the original XML.

Note the comment: Svelte Kit has the capability to apply server-side rendering to a service route like this. In a real application this could be used so that the Web API wouldn’t need to be called at runtime for this metadata. But of course the format conversion is a temporary solution, and for test purposes I keep everything dynamic by using this proxy service.

Create a second service source code file now with the following path name: src/routes/api/schema/[className]/[[lang]]/+server.js. For those unfamiliar with Svelte Kit: yes, include all the brackets in the path, just like that! They indicate parameters, which will be extracted from the route when a request comes in.

The full source code of the file is a bit too long and verbose to include here. If you are following along, please access the complete file at this URL and copy&paste the code from there. Additionally, please use pnpm i -D lodash to make the package lodash available to the project.

Here is the main function GET from this code file:

export async function GET({ fetch, params }) {
	const { className, lang = '' } = params;
	const namespace = className.slice(0, className.lastIndexOf('.'));
	const entityName = className.slice(className.lastIndexOf('.') + 1);

	const { propertyNames, idField } = await getPropertyInfo(fetch, namespace, entityName);

	const promises = [
		Promise.resolve({ $$idField: idField }),
		getClassCaptionPromise(fetch, className, lang),
		...propertyNames.map((pn) => getMemberCaptionPromise(fetch, className, pn, lang))
	];

	const result = await Promise.all(promises).then(mergeAll);

	return json(result);
}

The function receives the input parameters className and (optionally) lang from the routing system. Through the various helper functions, it calls the Web API services and builds a schema structure for the given type. With the new route active, a test renders output like this:

> curl http://localhost:5173/api/schema/XAFApp.Module.BusinessObjects.SaleProduct | jq
{ 
	"$$idField": "ID",
	"$$classCaption": "Sale Product",
	"ID": "ID",
	"Name": "Product Name",
	"Price": "Product Price"
}

As you can see, I decided to include the two fields $$idField and $$classCaption using a name format that is very unlikely to be used by actual data structure fields. Of course you could choose a different format in case that the $$ prefix collides with your real field names.

Fetch the schema from the page load function

Now it is time to call the new service and retrieve the schema as part of the data loading process that runs when the page is accessed. Edit src/routes/saleProducts/+page.server.js and change it to this:

export async function load({ fetch }) {
	const odataUrl = `http://webapi:5273/api/odata/SaleProduct`;

	const dataSource = fetch(odataUrl)
		.then((res) => {
			if (res.ok) return res;
			throw new Error(`HTTP error, status: ${res.status}`);
		})
		.then((res) => res.json())
		.then((res) => res.value);

	const schemaUrl = `/api/schema/XAFApp.Module.BusinessObjects.SaleProduct`;
	const schema = fetch(schemaUrl)
		.then((res) => {
			if (res.ok) return res;
			throw new Error(`HTTP error, status: ${res.status}`);
		})
		.then((res) => res.json());

	return await Promise.all([dataSource, schema])
		.then(([ds, sc]) => ({ dataSource: ds, schema: sc }))
		.catch((err) => {
			console.log(err);
			return { error: err.message };
		});
}

There are several changes compared to the previous version, which used a simple piece of code to load the data alone.

  • Error handling has been inserted, which is a best practice that makes it easier to track down issues, as the implementation grows in complexity
  • In addition to the original fetch call, a second promise is constructed to retrieve the schema data from the new service
  • Both promises are combined and awaited before the return, and the function is declared async. Technically, Svelte Kit can return the promises without awaiting them first, and it can even stream results, but in a future version one part of the results generated in this sample code will depend on user authorization and so it doesn’t make sense to return one part of the result set while a second part may not be returned — I set up the code now to allow for this change in the future.

Use the schema to display the correct captions

Edit src/routes/saleProducts/+page.svelte first. This is where the data is received after the load function returns it, and you need to receive the schema and error elements now that may be returned after the recent changes.

export let data;
$: ({ dataSource, schema, error } = data);

Now you can use the new schema information to include the correct type caption:

<script>
	...
	const classCaptionPlaceholder = 'Sale Products';
</script>

<h2 class="font-bold text-xl">
	Data: {schema ? schema['$$classCaption'] : classCaptionPlaceholder}
</h2>

In case the error field has been returned, it should be displayed — at least that is the approach this demo will use. In a real application you may decide differently, since end users don’t necessarily like technical error messages!

Make sure to include the schema parameter for the DataTable while you’re at it.

{#if error}
	<div class="error">{error}</div>
{:else}
	<DataTable {dataSource} {fields} {schema} />
{/if}

<style lang="postcss">
	.error {
		@apply font-bold border bg-red-200 p-2 rounded;
	}
</style>

Finally, edit src/lib/DataTable.svelte and use the incoming schema information to display the column headers:

<script>
	export let dataSource;
	export let fields;
	export let schema = {};
</script>

<table class="border-separate w-full">
	<tr>
		{#each Object.keys(fields) as f}
			<th class={fields[f].class}>{schema[f] || f}</th>
		{/each}
	</tr>
...

With all the changes in place, you will see the schema details reflected by the column headers and in the table caption.

Table showing column headers and caption

The beauty of this solution is that the JavaScript UI now reacts to XAFML changes dynamically. This makes future extensions easier, and it becomes possible to add localization features and end-user capabilities like the language chooser for XAF Blazor apps.

ASP.NET Core Blazor language switcher

Conclusion

Here is the link to the branch “stage-2” in the GitHub repository again. This branch includes all the changes described in this post.

Thank you for reading, or following along! In the next post of the series I will describe how to dynamically retrieve data to take advantage of sorting and filtering features provided by the Web API service.

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.