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.
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.

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).
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!

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.

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.

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.

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.
Your Feedback Matters!
Please take a moment to reply to the following questions – your feedback will help us shape/define future development strategies.