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.
You can find source code for each stage of the demo in the GitHub repository. There are now four branches, with stage 4 representing this post.
- Stage 1 — getting started, first data loading
- Stage 2 — Model-based caption customization
- Stage 3 — sort and filter data
- Stage 4 — editing and validation (for this post)
Persist Objects Via OData
The standard data manipulation interface made available by the Web API Service uses the OData standard. Microsoft has comprehensive documentation on the various endpoints and URL structures that are used by this protocol. You can also find dozens of code examples in our Web API Service documentation as well: Make HTTP Requests to the Web API. In this step of the sample project I will show the use of a POST
call to create a new entity, and a PATCH
call to modify an entity.
You can try these calls from the command line, since they are basic HTTP requests. Alternatively, as I described previously in the second part of this blog series, UI clients are available to make testing of HTTP APIs easier. In all cases, the information published by the Swagger interface at http://localhost:5273/swagger
in the sample project is useful to list the available endpoints and find out the correct syntax.
Here is an example session at the command line, which may help you get started with your own explorations. First, you see all entities of type SaleProduct
queried from the backend. Then, a new SaleProduct
is created using a POST
call, modified with a PATCH
call and then queried individually to confirm the modification.
Note that jq
is a convenient command line tool that handles JSON — invoked without options, it simply formats its input nicely so it’s easier to read. It also has powerful features to query and reshape JSON, so I recommend you check it out!
> curl http://localhost:5273/api/odata/SaleProduct | jq
{
"@odata.context": "http://localhost:5273/api/odata/$metadata#SaleProduct",
"value": [
{
"ID": "08d7df5d-ca05-48f3-66fc-08db35e6b738",
"Name": "Rubber Chicken",
"Price": 13.99
},
{
"ID": "078b4ebe-d6b3-43ce-66fd-08db35e6b738",
"Name": "Pulley",
"Price": 3.99
},
{
"ID": "e135b865-eac2-46dc-66fe-08db35e6b738",
"Name": "Starship Enterprise",
"Price": 149999999.99
},
{
"ID": "68753142-114f-49d3-66ff-08db35e6b738",
"Name": "The Lost Ark",
"Price": 1000000000000
}
]
}
> curl -X POST http://localhost:5273/api/odata/SaleProduct \
-H "Content-Type: application/json" \
-d '{ "Name": "Test Item", "Price": 17.99 }' | jq
{
"@odata.context": "http://localhost:5273/api/odata/$metadata#SaleProduct/$entity",
"ID": "c67c4dcf-4327-4ed4-ddb7-08db46429611",
"Name": "Test Item",
"Price": 17.99
}
> curl -X PATCH http://localhost:5273/api/odata/SaleProduct/c67c4dcf-4327-4ed4-ddb7-08db46429611 \
-H "Content-Type: application/json" \
-d '{ "Name": "Test Item - changed" }' | jq
> curl http://localhost:5273/api/odata/SaleProduct/c67c4dcf-4327-4ed4-ddb7-08db46429611 | jq
{
"@odata.context": "http://localhost:5273/api/odata/$metadata#SaleProduct/$entity",
"ID": "c67c4dcf-4327-4ed4-ddb7-08db46429611",
"Name": "Test Item - changed",
"Price": 17.99
}
Use a Form to Edit Data
Svelte has very good support for edit forms, so the syntax does not need to be very complex for the edit form implementation. Note that no client-side validation is applied here — it would be an extra job for the developer to add this. For the sample I’m going to rely on the server-side validation that is supported directly by the Web API service.
Here is the complete code for the form. If you are following along, create a new file src/routes/saleProducts/edit/[[productId]]/+page.svelte
and insert this code.
<script>
import { enhance } from '$app/forms';
export let data;
$: ({ product, schema } = data);
export let form;
</script>
{#if product.ID}
<h2>Edit {schema['$$classCaption']} "{product.Name}"</h2>
{:else}
<h2>Create new product</h2>
{/if}
<form method="post" use:enhance>
<input type="hidden" name="ID" value={product.ID || ''} />
<table class="border-y-2 w-full">
<tr>
<td><label for="name">{schema.Name}</label></td>
<td><input type="text" id="name" name="Name" bind:value={product.Name} /></td>
</tr>
<tr>
<td><label for="price">{schema.Price}</label></td>
<td>
<input class="text-right" type="number"
id="price" name="Price" step={0.01}
bind:value={product.Price} />
</td>
</tr>
</table>
{#if form?.error}
<div class="bg-red-200 rounded w-full p-2 m-2 whitespace-pre-line">{form.error}</div>
{/if}
<div class="flex mt-4">
<button class="ml-auto bg-green-200 px-4 py-1 rounded hover:bg-red-200" type="submit">
Save
</button>
</div>
</form>
<style lang="postcss">
input {
@apply border px-2 w-full;
}
label {
@apply mx-2;
}
td {
@apply py-2;
}
</style>
The form is basic, but the implementation takes advantage of Svelte-specific features. The script
block exports a form
property in addition to the usual data
property. This is used automatically in the submit cycle, to send data back to the form, if necessary. In this sample, it is used to carry error
values, which are evaluated in the bottom part of the form. It is possible to use the same mechanism to return other details to the form, as required.
Together with the general topic of progressive enhancement, the details of the submit cycle are explained on this page of the Svelte Kit documentation. The enhance
action, which you can see applied to the <form>
tag in code, enables some the advanced enhancement features.
The conditional blocks in code distinguish between the case where the product
object includes an ID
, and the case where it doesn’t — the former occurs when an existing item is edited, the latter when a new item is created. In the path you created for this Svelte component, the syntax [[productId]]
denotes that the path parameter productId
is optional. OData does not allow a POST
call to include an ID
value for a newly created object, so we start out without an ID
. When the ID
is known after creation, it can be added to the URL.
The final interesting detail you may notice is that the <form>
tag declares method="post"
, but no action
attribute is specified. You may imagine that Svelte Kit has a default for the POST
callback to the server, like other frameworks do — specifically ASP.NET uses similar approaches. This is exactly what happens, and since the form includes just one submit
button, no further configuration is necessary. The Svelte Kit documentation includes details about named actions, which you need in case you want to trigger multiple actions from the same form.
Server Code to Load a SaleProduct
In the same path as the last +page.svelte
, add a file called +page.server.js
now. Then create the load
function, like this:
export function load({ fetch, params }) {
let product = { Name: '', Price: 0.0 };
if (params.productId) {
product = fetch(`http://webapi:5273/api/odata/SaleProduct/${params.productId}`).then((res) =>
res.json()
);
}
const schema = fetch('/api/schema/XAFApp.Module.BusinessObjects.SaleProduct').then((res) =>
res.json()
);
return { product, schema };
}
This function includes most of the logic you may have missed in the component. It is called by the framework when data has to be loaded — obviously this occurs when the page is activated, but it can also occur ahead of time when the user hovers the mouse over a link, or during server-side rendering.
As you know, the productId
is optional. A default product
is created and potentially returned, but if a productId
is specified in the URL, the corresponding product is fetched from the Web API Service instead.
The return
statement combines the data object with the schema. Since you prepared this functionality as part of the second post in this blog series, it is easy to reuse now. I didn’t point this out, but the schema details are used in the edit form to render the field labels, so the form is consistent with the column headers in the overview page.
Note that Svelte Kit applies special handling to the top level elements of the returned structure. If they are promises, they are awaited automatically before being returned. That’s important in this example, because it means that product
is always a simple data structure, even though it could be either a plain object or a promise based on the code in the load
function alone. For more details about promises in Svelte Kit return values, see this documentation page.
Finally, to point out the possibly obvious: the return value is fed into the property data
of the page component. You have seen this before so it shouldn’t come as a surprise, but in this case the data flow is more complicated than before due to the bit that’s missing so far: the form action.
Add a Form Action to Save an Entity
Once more in src/routes/saleProducts/edit/[[productId]]/+page.server.js
, add the form action default
using the following code:
export const actions = {
default: async ({ request, fetch }) => {
const formData = Object.fromEntries(await request.formData());
const postData = { Name: formData.Name, Price: parseFloat(formData.Price) || 0 };
if (!formData.ID) {
// POST a new product
} else {
// PATCH an existing product
}
}
};
The function default
is the action that will be called by Svelte Kit if a <form>
is submitted that does not include an action
attribute. If you use named actions in more complex cases, you will simply have more than one item instead of default
.
Each action function receives an event
parameter, which includes the parts you would expect: the request
object, parameters and route info, access to cookies and headers, and the usual fetch
reference you can see in code. Read the form data from the request as shown, and there may be an ID
included or not — this distinguishes whether a new entity is being created or an existing one modified. The two cases have a few details in common, but there are important differences so I’ll show them separately. Here is the code to create a new entity:
if (!formData.ID) {
const response = await fetch(`http://webapi:5273/api/odata/SaleProduct`, {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify(postData)
});
if (response.ok) {
const json = await response.json();
throw redirect(303, `/saleProducts/edit/${json.ID}`);
} else {
return fail(400, { error: await response.text() });
}
}
The postData
is sent with the fetch
request. Then, if everything has gone well, the response
object provides access to the newly generated ID
— compare to the command line example above. Using this ID, a redirect
is executed (in Svelte Kit this happens by way of an exception) so that the same edit form is displayed again, but this time for the existing object with the new ID
.
The error handling takes advantage of the form
property in the page component, which I mentioned before. The value object returned with the fail
helper ends up in the form
property. It is up to you which information you want to send to the form in this case, and some common patterns return the complete formData
for further evaluation. This is not necessary in this case, but it’s interesting to see how flexible this simple mechanism is.
The second block of code deals with the modification of an entity. Here it is:
...
} else {
const response = await fetch(`http://webapi:5273/api/odata/SaleProduct/${formData.ID}/`, {
method: 'PATCH',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify(postData)
});
if (response.ok) {
return null;
} else {
return fail(400, { error: await response.text() });
}
}
One difference is the use of the PATCH
HTTP verb, but the handling of the success case is also different: it returns null because error information is not required in this case. Since the null
result is returned as a success value, the default handling on the client is to reload the page. The user can continue editing in a new instance of the form. Note that the details of this behavior depend on the enhance
function mentioned above, and you can customize them as needed.
Navigate to the Edit Form
A few additions to the component in src/lib/DataTable.svelte
are required so that users can navigate to the new edit form, both to create new SaleProduct
entities and to modify existing ones. I decided to add a new column to the table that can work as a “command column”. The “new” and “edit” buttons can then be added to that column, leaving room for other extensions.
For flexibility, add the property editBaseUrl
to the component first:
...
export let dataSource;
export let fields;
export let schema = {};
export let displayState = {
sort: undefined,
desc: false,
filters: {}
};
export let editBaseUrl;
...
Add the highlighted blocks to the table
rendering of the component:
<table class="border-separate w-full">
<tr>
{#each Object.keys(fields) as f}
...
{/each}
<th class="action">
{#if editBaseUrl}
<a href={editBaseUrl} alt="Create"><span class="fa fa-star-o" /></a>
{/if}
</th>
</tr>
<tr class="filterRow">
{#each Object.keys(fields) as f}
...
{/each}
<td class="action" />
</tr>
{#await dataSource}
<tr class="placeholder"><td colspan="2">Waiting for data</td></tr>
{:then dataSourceContent}
{#if dataSourceContent && Array.isArray(dataSourceContent)}
{#each dataSourceContent as item}
<tr>
{#each Object.keys(fields) as f}
<td class={fields[f].class}>{item[f]}</td>
{/each}
<td class="action">
{#if editBaseUrl}
<a href="{editBaseUrl}/{item[schema['$$idField']]}" alt="Edit"
><span class="fa fa-edit" /></a
>
{/if}
</td>
</tr>
...
Now include the following code in the style
block at the bottom of the file:
...
.action {
@apply bg-green-200 text-center;
}
.action a {
@apply border-2 rounded bg-white px-2 py-0.5 hover:bg-orange-200;
}
</style>
Finally, edit src/routes/saleProducts/+page.svelte
and pass the property editBaseUrl
to the component:
...
<DataTable
{dataSource}
{fields}
{schema}
{displayState}
on:displayStateChanged={displayStateChanged($page.url.pathname)}
editBaseUrl="/saleProducts/edit"
/>
...
Create and Edit Sale Products
The new functionality is now ready to try. In the top right corner of the data table, click the New button to create a new entity.
You’ll see that the URL addresses the edit form, but it does not include an ID
at this point. Enter some test data, click Save, and the form will be redirected to a URL that includes the new ID
while the edit content remains unchanged.
Navigate back to the overview page and edit an item to be sure that the mechanism works fully!
Utilize Server-Side Validation
The Web API service can take advantage of validation by using the XAF Validation Module. This is a powerful rule-based system which includes many predefined rule types and the option to create custom ones. It covers simple value-based validation techniques as well as complex structural concerns. All rules can be applied either from code, using attributes, or by editing the Application Model as I described in second part of this blog series. Note that the XAF Validation Module requires a DevExpress Universal Subscription.
.png)
Detailed instructions to enable the Validation Module for your Web API service can found in the documentation. The following description is complete for the sample project and replicates parts of the documentation.
Begin by adding the required NuGet packages to your projects. For the Module
and WebApi
projects, add DevExpress.ExpressApp.Validation
. If you’d like to test validation with the Blazor app as well, use the package DevExpress.ExpressApp.Validation.Blazor
.
Note that if you are running the sample setup in Docker as I suggested previously, you need to restart the server-side projects before the project file changes are recognized correctly. I recommend to do this once, after you’ve applied all changes described below. By the way, the Svelte app does not need to restarted — it’s ready to go, as it is!
Edit the file Module.cs
in your Module
project. Add a line to the constructor code, so that the Validation Module is included in the module setup.
RequiredModuleTypes.Add(typeof(DevExpress.ExpressApp.SystemModule.SystemModule));
RequiredModuleTypes.Add(typeof(DevExpress.ExpressApp.Objects.BusinessClassLibraryCustomizationModule));
RequiredModuleTypes.Add(typeof(DevExpress.ExpressApp.Validation.ValidationModule));
Add the service implementation file to the Module
project as Services/ValidatingDataService.cs
. This is the standard implementation from the docs, which can be used unchanged in many cases. On the other hand, remember that you added this yourself, since it’s also possible to make changes here if you’d like validation to be applied differently for your project.
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Core;
using DevExpress.ExpressApp.DC;
using DevExpress.ExpressApp.WebApi.Services;
using DevExpress.Persistent.Validation;
namespace XAFApp.WebApi.Core;
public class ValidatingDataService : DataService {
readonly IValidator validator;
public ValidatingDataService(IObjectSpaceFactory objectSpaceFactory,
ITypesInfo typesInfo, IValidator validator)
: base(objectSpaceFactory, typesInfo) {
this.validator = validator;
}
protected override IObjectSpace CreateObjectSpace(Type objectType) {
IObjectSpace objectSpace = base.CreateObjectSpace(objectType);
objectSpace.Committing += ObjectSpace_Committing;
return objectSpace;
}
private void ObjectSpace_Committing(object? sender,
System.ComponentModel.CancelEventArgs e) {
IObjectSpace os = (IObjectSpace)sender!;
var validationResult = validator.RuleSet.ValidateAllTargets(
os, os.ModifiedObjects, DefaultContexts.Save
);
if (validationResult.ValidationOutcome == ValidationOutcome.Error) {
throw new ValidationException(validationResult);
}
}
}
In the file Startup.cs
in the Module
project, add a line that makes the new service available to the ASP.NET Core infrastructure.
services.AddScoped<IDataService, XAFApp.WebApi.Core.ValidatingDataService>();
services
.AddXafWebApi(Configuration, options => {
...
Now everything is set up, and you only need to add validation rules. The easiest way to do this is to add attributes to your persistent types. For instance, the following code applies several rules to the two properties of the sample data type:
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using DevExpress.Persistent.Validation;
namespace XAFApp.Module.BusinessObjects {
[DefaultClassOptions]
public class SaleProduct : BaseObject {
public SaleProduct() {
}
[RuleUniqueValue]
[RuleRequiredField]
public virtual string Name { get; set; }
[RuleRequiredField]
[RuleValueComparison(ValueComparisonType.GreaterThan, 0)]
public virtual decimal? Price { get; set; }
}
}
Please note that there are many documentation topics that cover validation rules in detail. For instance, this page describes everything you need to know about applying rules in code, or by using the Model Editor in Visual Studio. If you would like to implement custom rules, read this documentation page.
With the rules established, now is the time to make sure that your Docker services have been restarted, and that there are no errors in the logs. Then you can attempt to edit data again in the JavaScript frontend, and you will see error messages appear if you violate the validation rules.
This is impressive functionality! There are aspects we plan to improve in the future. For instance, it would be good to include endpoints out of the box that could be used to validate data during the editing process, independently of of the submit
button. It is possible to create such endpoints manually now, but we will make this easier.
Conclusion
As usual, here is the link to the GitHub branch for this post: “stage-4”.
Thank you for reading and following along! Next time I will add authentication to the application, and a future post will also cover retrieval of reports.
Your Feedback Matters!
Please take a moment to reply to the following questions – your feedback will help us shape/define future development strategies.