Web Applications with TMS Web Core
TMS Web Core is the TMS Software framework for building web applications using Delphi. It allows you to create pure HTML/JS Single-Page-Applications that runs in your browser.
Even though the web application generated by TMS Web Core can run 100% stand alone in the browser, in many scenarios it needs data to work with, and such data needs to be retrieved from a server (and eventually sent back for modifications). Usually this data communication is done through a REST API Server - the web client performs requests using HTTP(S), and send/receive data in JSON format.
TMS XData is the ideal back-end solution for TMS Web Core. It not only allows you to create REST API servers from an existing database in an matter of minutes, but is also provides the most complete and high-level client-side framework for TMS Web Core, including Delphi design-time support with visual components, a TXDataWebClient component that abstracts low-level HTTP/JSON requests in a very easy and high-level way of use, and a dataset-like optional approach that simply feels home to Delphi users but still uses modern REST/JSON requests behind the scenes.
The following topics cover in details how to use TMS XData Web-Client Framework and make use of TMS XData servers from TMS Web Core applications.
Setting Up the Connection with TXDataWebConnection
TXDataWebConnection is the first (and key) component you need to use to connect to XData server from TMS Web Core applications. Simply drop the component in the form set its URL property to the root URL of XData server:
XDataWebConnection1.URL := 'http://localhost:2001/tms/music';
Tip
Even though the examples above and below will show setting properties from code, since TXDataWebConnection is available at design-time, you can set most of the properties described here at design-time in object inspector, including testing the connection.
Then you perform the connection setting Connected property to true:
DataWebConnection1.Connected := True;
It's as simple as that. However, for web applications, you must be aware that all connections are performed asynchronously. This means that you can't be sure when the connection will be performed, and any code after Connected is set to true is not guaranteed to work if it expects the connection to be established. In the following code, for example, the second line will probably not work because the connection is probably not yet finished:
XDataWebConnection1.Connected := True;
// The following line will NOT work because connection
// is still being established asynchronously
PerformSomeRequestToXDataServer();
To make sure the component is connected and you can start performing requests to XData server, you should use OnConnect and OnError events:
From TMS Web Core 1.6 and on, you can also use the OpenAsync
method using the await
keyword. This will ensure the next line will be executed after OpenAsync
is complete, even though it's executed asynchronously:
await(XDataWebConnection1.OpenAsync);
// The following line will be executed
// after the connection asynchronously established
PerformSomeRequestToXDataServer();
OnConnect and OnError events
When connecting, either one of those two events will be fired upon request complete. Use the OnConnect event to be sure the connection was performed and start communicating with XData server:
procedure TForm1.ConnectButtonClick(Sender: TObject);
begin
XDataWebConnection1.URL := 'http://localhost:2001/tms/music';
XDataWebConnection1.OnConnect := XDataWebConnection1Connect;
XDataWebConnection1.OnError := XDataWebConnection1Error;
XDataWebConnection1.Connected := True;
end;
procedure TForm1.XDataWebConnection1Connect(Sender: TObject);
begin
WriteLn('XData server connected succesfully!');
PerformSomeRequest;
end;
procedure TForm1.XDataWebConnection1Error(Error: TXDataWebConnectionError);
begin
WriteLn('XData server connection failed with error: ' + Error.ErrorMessage);
end;
Open method
As an alternative to events, you can connect using Open method, which you pass two parameters: a callback for success and a callback for error.
procedure TForm1.ConnectButtonClick(Sender: TObject);
procedure OnConnect;
begin
WriteLn('XData server connected succesfully!');
PerformSomeRequest;
end;
procedure OnError(Error: TXDataWebConnectionError);
begin
WriteLn('XData server connection failed with error: ' + Error.ErrorMessage);
end;
begin
XDataWebConnection1.URL := 'http://localhost:2001/tms/music';
XDataWebConnection1.Open(@OnConnect, @OnError);
end;
As stated previously, OpenAsync
is the equivalent to Open
that can be used with await
:
procedure TForm1.ConnectButtonClick(Sender: TObject);
begin
XDataWebConnection1.URL := 'http://localhost:2001/tms/music';
try
await(XDataWebConnection1.OpenAsync);
WriteLn('XData server connected succesfully!');
PerformSomeRequest;
except
on Error: Exception do
WriteLn('XData server connection failed with error: ' + Error.ErrorMessage);
end;
end;
OnRequest event
OnRequest event is called before every request about to be sent to the XData server. This is an useful event to modify all requests at once. For example, it's often used to add authentication information the request, like a authorization token with a JWT header:
procedure TForm1.XDataWebConnection1Request(Request: TXDataWebConnectionRequest);
begin
Request.Request.Headers.SetValue('Authorization', 'Bearer ' + LocalJWTToken);
end;
DesignData property
DesignData property is intended to be used just at design-time, as an opportunity for you to add custom headers to be sent to the server. Analogously to the OnRequest event, it's useful to add authorization header to the request so you can connect to XData server at design-time. To use it just click the ellipsis button in the DesignData.Headers property and add the headers you need.
Those headers will by default not be saved in the DFM, meaning you will lose that information if you close/open the project, or even the form unit. This is for security purposes. In the exceptional case you want information to be saved (and thus also loaded at runtime), you can set the Persist property to True.
Using TXDataWebClient
After setting up the connection, you can use TXDataWebClient component to communicate with the TMS XData Server from a TMS Web Core application. Note that the TXDataWebConnection must be previously connected. Performing operations with TXDataWebClient won't automatically setup the connection.
TXDataWebClient perform operations similar to the ones performed by TXDataClient, used in Delphi desktop and mobile applications. It means you can retrieve single and multipe entities (GET), insert (POST), update (PUT), delete (DELETE) and also invoke service operations.
However, there are several differences. The first and main one is that all requests are performed asynchronously. This means that when you call TXDataWebClient methods, you won't have a function result provided immediately to you, but instead you have to use event or callback to receive the result. Here is, for example, how to retrieve an entity (GET request) from server, more specifically retrieve an object artist, from entity set "Artist", with id 1:
procedure TForm1.GetArtistWithId1;
begin
XDataWebClient1.Connection := XDataWebConnection1;
XDataWebClient1.OnLoad := XDataWebClient1Load;
XDataWebClient1.Get('Artist', 1);
end;
procedure TForm1.XDataWebClient1Load(Response: TXDataClientResponse);
var
Artist: TJSObject;
begin
// Both lines below are equivalent.
Artist := TJSObject(Response.Result);
Artist := Response.ResultAsObject;
// Use Artist object
end;
First, associate the TXDataWebClient component to an existing TXDataWebConnection component which will has the connection settings. This can be also done at design-time.
Second, set the OnLoad event of the component and add the code there to be executed when request is completed. Also can be done at design-time.
Finally, execute the method that perform the operation. In this case, Get method, passing the entity set name (Artist) and the id (1).
When the request is complete, the OnLoad event will be fired, and the result of the request (if it does return one) will be available in the Response.Result property. That property is of type JSValue, which can be any valid value. You will have to interpret the result depending on the request, if you are retrieving a single entity, that would be a TJSObject. If you are retrieving a list of objects, then the value can be a TJSArray, for example. You can alternatively use ResultAsObject or ResultAsArray.
TXDataWebClient component is very lightweight, so you can use as many components as you want. That means that you can drop one TXDataWebClient component in the form for each different request you want to perform, so that you will have one OnLoad event handler separated for each request. This is the more RAD and straightforward way to use it. In the case you want to use a single web client component for many requests, you can differentiate each request by the request id.
New async/await mechanism:
As of TMS Web Core 1.6, you can also use the Async
version of all methods. It's the same name but with the Async
suffix, and you can then use the await
keyword so you don't need to work with callbacks:
procedure TForm1.GetArtistWithId1;
var
Response: TXDataClientResponse;
Artist: TJSObject;
begin
XDataWebClient1.Connection := XDataWebConnection1;
Response := await(XDataWebClient1.GetAsync('Artist', 1));
// Both lines below are equivalent.
Artist := TJSObject(Response.Result);
Artist := Response.ResultAsObject;
// Use Artist object
end;
Using RequestId
Each response in the OnLoad event will have a request id. By default the request id is the name of the performed operation, in lowercase. So it will be "get" for get requests, "list" for list requests, etc.:
procedure TForm1.XDataWebClient1Load(Response: TXDataClientResponse);
var
Artist: TJSObject;
begin
if Response.RequestId = 'get' then
begin
Artist := TJSObject(Response.Result);
end;
end;
If you want to change the default request id, you can pass a different one as an extra parameter to the request, and check for it in the OnLoad event:
XDataWebClient1.Get('Artist', 1, 'get artist');
Handling errors
If you don't specify anything, all request errors that might happen will fire the OnError event of the associated TXDataWebConnection component. This way you can have a centralized place to handle all request errors for that XData server.
But if you want to add error-handling code that is specific to a TXDataWebClient connection, TXDataWebClient also provides an OnError event:
procedure TForm1.XDataWebClient1Error(Error: TXDataClientError);
begin
WriteLn('Error on request: ' + Error.ErrorMessage);
end;
When you use the Async
methods, all you have to do is wrap the code in a try..except block:
try
Resonse := await(XDataWebClient1.GetAsync('Artist', 1));
except
on E: Exception do ; // do something with E
end;
Using callbacks
Alternatively to using OnLoad and OnError events, you can use the request methods passing callback as parameter. You can pass a callback for successful response, and optionally an extra callback for error response (if you don't pass the error callback, it will fallback to the OnError event).
procedure TForm1.GetArtistWithId1;
procedure OnSuccess(Response: TXDataClientResponse);
var
Artist: TJSObject;
begin
Artist := TJSObject(Response.Result);
// Use Artist object
end;
procedure OnError(Error: TXDataClientError);
begin
WriteLn('Error on request: ' + Error.ErrorMessage);
end;
begin
XDataWebClient1.Get('Artist', 1, @OnSuccess, @OnError);
end;
Available request methods
The following is a list of available request methods in TXDataWebClient.
Remember that the method signatures in this list include only the
required parameters. You can always also use either RequestId parameter
or the callback parameters, as previously explained.
Also, remember that all those methods have an "async/await version" that you can use, just by suffixing the method name with the Async
suffix: GetAsync
, PostAsync
, etc.
Name | Description |
---|---|
procedure Get(const EntitySet: string; Id: JSValue) | Retrieves a single entity from the server (GET request), from the specified entity set and with the specified id. Result will be a TJSObject value. |
procedure Get(const EntitySet, QueryString: string; Id: JSValue) | Retrieves a single entity from the server (GET request), from the specified entity set and with the specified id. Optionally you can provide a QueryString parameter that must contain query options to added to the query part of the request. For get requests you would mostly use this with $expand query option. Result will be a TJSObject value. |
procedure List(const EntitySet: string; const Query: string = '') | Retrieves a collection of entities from the specified entity set in the server (GET request). You can provide a QueryString parameter that must contain query options to added to the query part of the request, like $filter, $orderby, etc.. Result will be a TJSArray value. |
procedure Post(const EntitySet: string; Entity: TJSObject) | Inserts a new entity in the entity set specified by the EntitySet parameter. The entity to be inserted is provided in the Entity parameter. |
procedure Put(const EntitySet: string; Entity: TJObject) | Updates an existing entity in the specified entity set. You don't need to provide an id separately since the Entity parameter should already contain all the entity properties, including the correct id. |
procedure Delete(const EntitySet: string; Entity: TJObject) | Deletes an entity from the entity set. The id of the entity will be retrieved from the Entity parameter. Since this is a remove operation, only the id properties are relevant, all the other properties will be ignored. |
procedure RawInvoke(const OperationId: string; Args: array of JSValue) | Invokes a service operation in the server. The OperationId identifies the operation and Args contain the list of parameters. More info below. |
Invoking service operations
You can invoke service operations using RawInvoke methods. Since you can't use the service contract interfaces in TMS Web Core yet, the way to invoke is different from TXDataClient. The key parameter here is OperationId, which identifies the service operation to be invoked. By default, it's the interface name plus dot plus method name.
For example, if in your server you have a service contract which is an interface named "IMyService" which contains a method "Hello", that receives no parameter, you invoke it this way:
XDataWebClient1.RawInvoke('IMyService.Hello', []);
If the service operation provides a result, you can get it the same way as described above: either using OnLoad event, or using callbacks:
procedure TForm2.WebButton1Click(Sender: TObject);
procedure OnResult(Response: TXDataClientResponse);
var
GreetResult: string;
begin
GreetResult := string(TJSObject(Response.Result)['value']);
end;
begin
Client.RawInvoke('IMyService.Greet', ['My name'], @OnResult);
end;
Since we're in the web/JavaScript world, you must know in more details how results are returned by service operations in JSON format.
As the example above also illustrates, you can pass the operation parameters using an array of JSValue values. They can be of any type, inculding TJSObject and TJSArray values.
Just as the methods for the CRUD endpoints, you also have a RawInvokeAsync
version that you can use using await
mechanism:
procedure TForm2.WebButton1Click(Sender: TObject);
var
Response: TXDataClientResponse
GreetResult: string;
begin
Response := await(Client.RawInvokeAsync('IMyService.Greet', ['My name']);
GreetResult := string(TJSObject(Response.Result)['value']);
end;
Other properties
- property ReferenceSolvingMode: TReferenceSolvingMode
Specifies how $ref occurrences in server JSON response will be handled.- rsAll: Will replace all $ref occurrences by the instance of the referred object. This is default behavior and will allow dealing with objects easier and in a more similar way as the desktop/mobile TXDataClient. It adds a small overhead to solve the references.
- rsNone: $ref occurrences will not be processed and stay as-id.
Using TXDataWebDataset
In addition to TXDataWebClient, you have the option to use TXDataWebDataset to communicate with TMS XData servers from web applications. It's even higher level of abstraction, at client side you will mostly work with the dataset as you are used to in traditional Delphi applications, and XData Web Client framework will translate it into REST/JSON requests.
Setting it up is simple:
1. Associate a TXDataWebConnection to the dataset through the Connection property (you can do it at design-time as well). Note that the TXDataWebConnection must be previously connected. Performing operations with TXDataWebDataset won't automatically setup the connection.
XDataWebDataset1.Connection := XDataWebConnection1;
2. Set the value for EntitySetName property, with the name of the entity set in XData server you want to manipulate data from. You can also set this at design-time, and object inspector will automatically give you a list of available entity set names.
XDataWebDataset1.EntitySetName := 'artist';
3. Optionally: specify the persistent fields at design-time. As with any dataset, you can simply use the dataset fields editor and add the desired fields. TXDataWebDataset will automatically retrieve the available fields from XData server metadata. If you don't, as with any dataset in Delphi, the default fields will be created.
And your dataset is set up. You can the use it in several ways, as explained below.
Loading data automatically
Once the dataset is configured, you just need to call Load method to retrieve data:
XDataWebDataset1.Load;
This will perform a GET request in XData server to retrieve the list of entities from the specific entity set. Always remember that such requests are asynchronous, and that's why you use Load method instead of Open. Load will actually perform the request, and when it's finished, it will provide data to the dataset and only then, call Open method. Which in turn will fire AfterOpen event. If you want to know when the request is finished and data is available in the dataset, use the AfterOpen event.
Note
If you already have data in the dataset and want Load
method to fully update existing data,
make sure the dataset is closed before calling Load
method.
To filter out results, you can (and should) use the QueryString property, where you can put any query option you need, including $filter and $top, which you should be use to filter out the results server-side and avoiding retrieving all the objects to the client. Minimize the number of data sent from the server to the client!
XDataWebDataset1.QueryString := '$filter=startswith(Name, ''John'')&$top=50';
XDataWebDataSet1.Load;
Paging results
The above example uses a raw query string that includes "&$top=50" to retrieve only 50 records. You can do paging that way, by building the query string accordingly. But TXDataWebDataset provides additional high-level properties for paging results in an easier way. Simply use QueryTop and QuerySkip property to define the page size and how many records to skip, respectively:
XDataWebDataset1.QueryTop := 50; // page size of 50
XDataWebDataset1.QuerySkip := 100; // skip first 2 pages
XDataWebDataset1.QueryString := '$filter=startswith(Name, ''John'')';
XDataWebDataSet1.Load;
The datase also provides a property ServerRecordCount which might contain the total number of records in server, regardless of page size. By default, this information is not retrieved by the dataset, since it requires more processing at server side. To enable it, set ServerRecordCountMode:
XDataWebDataset1.ServerRecordCountMode := smInlineCount;
When data is loaded from the dataset (for example in the AfterOpen event), you can read ServerRecordCount property:
procedure TForm4.XDataWebDataSet1AfterOpen(DataSet: TDataSet);
begin
TotalRecords := XDataWebDataset1.ServerRecordCount;
end;
Loading data manually
Alternatively you can simply retrieve data from the server "manually" using TXDataWebClient (or even using raw HTTP requests, if you are bold enough) and provide the retrieved data to the dataset using SetJsonData. Since the asynchronous request was already handled by you, in this case where data is already available, and you can simply call Open after setting data:
procedure TForm1.LoadWithXDataClient;
procedure OnSuccess(Response: TXDataClientResponse);
begin
XDataWebDataset1.SetJsonData(Response.Result);
XDataWebDataset1.Open;
end;
begin
XDataWebClient1.List('artist', '$filter=startswith(Name, ''New'')', @OnSuccess);
end;
Modifying data
When using regular dataset operations to modify records (Insert, Append, Edit, Delete, Post), data will only be modified in memory, client-side. The underlying data (the object associated with the current row) will have its properties modified, or the object will be removed from the list, or a new object will be created. You can then use those modified objects as you want - manually send changes to the server, for example.
But TXDataWebDataset you can have the modifications to be automatically and transparently sent to the server. You just need to call ApplyUpdates:
XDataWebDataset1.ApplyUpdates;
This will take all the cached modifications (all objects modified, deleted, inserted) and will perform the proper requests to the XData server entity set to apply the client modifications in the server.
Other properties
Name | Description |
---|---|
SubPropsDepth: Integer | Allows automatic loading of subproperty fields. When adding persistent fields at design-time or when opening the dataset without persistent fields, one TField for each subproperty will be created. By increasing SubpropsDepth to 1 or more, dataset will also automatically include subproperty fields for each property in each association, up to the level indicated by SubpropsDepth. For example, if SubpropsDepth is 1, and the entity type has an association field named "Customer", the dataset will also create fields like "Customer.Name", "Customer.Birthday", etc.. Default is 0 (zero). |
CurrentData: JSValue | Provides the current value associated with the current row. Even though CurrentData is of type JSValue for forward compatibility, for now CurrentData will always be a TJSObject (an object). |
EnumAsIntegers: Boolean | This property is for backward compatibility. When True, fields representing enumerated type properties will be created as TIntegerField instances. Default is False, meaning a TStringField will be created for the enumerated type property. In XData, the JSON representing an entity will accept and retrieve enumerated values as strings. |
Solving Errors
In the process of solving errors in web applications, it's important to always check the web browser console, available in the web browser built-in developer tools. Each browser has its own mechanism to open such tools, in Chrome and Firefox, for example, you can open it by pressing F12 key.
The console gives you detailed information about the error, the call stack, and more information you might need to understand what's going on (the HTTP(S) requests the browser has performed, for example).
You should also have in mind that sometimes the web application doesn't even show a visible error message. Your web application might misbehave, or do not open, and no clear indication is given of what's going on. Then, whenever such things happen or you think your application is not behaving as it should, check the web browser console.
Here we will see common errors that might happen with web applications that connect to XData servers.
Error connecting to XData server
This is the most common error when starting a Web Core application using XData. The full error message you might get is the following:
XDataConnectionError: Error connecting to XData server | fMessage::XDataConnectionError:
Error connecting to XData server fHelpContext::0
And it will look like this:
As stated above, the first thing you should is open the browser console to check for more details - the reason for the connection to fail. There are two common reasons for that:
CORS issue
The reason for the error might be related to CORS if in the browser console you see a message like this:
This is caused because your web application files is being served from one host (localhost:8000 in the example above), and the API the app is trying to access is in a different host (localhost:2001 in the example).
To solve it, you have two options:
Modify either your web app or API server URL so both are in the same host. In the example above, run your web application at address localhost:2001, or change your XData API server URL to localhost:8000/tms/xdata.
Add CORS middleware to your XData server.
HTTPS/HTTP issue
One second common reason for wrong connection is a mix of HTTPS and HTTP connections. This usually happens when you deploy your Web Core application to a server that provides the files through HTTPS (using an SSL certificate), but your XData server is still at HTTP. Web browsers do not allow a web page served through HTTPS to perform Javascript requests using HTTP.
The solution in this case is:
Use HTTPS also in your XData server. It's very easy to associate your SSL certificate to your XData server. If you don't have an SSL certificate and don't want to buy one, you can use a free Let's Encrypt certificate in your XData server.
Revert your web app back to an HTTP server, so both are served through HTTP. Obviously, for production environments, this is not a recommended option.
XData Web Application Wizard
TMS XData provides a great wizard to scaffold a full TMS Web Core web client application.
This wizard will connect to an existing XData server and extract meta information it with the list of entities published by the server. With that information, it will create a responsive, Boostrap-based TMS Web Core application that will serve as a front-end for listing entities published by the server. The listing features include data filtering, ordering and paging.
To launch the wizard, go to Delphi menu File > New > Other..., then from the New Items dialog choose Delphi Projects > TMS Business, and select wizard "TMS XData Web Application".
Click Ok and the wizard will be launched:
In this first page you must provide the URL address of a running XData server. You can click the "Test Connection" button to check if the connection can be established. If the server requires authentication or any extra information sent by the client, you can use the "Set Request Headers..." button to add more HTTP headers to the client request (for example, adding a JWT token to an Authorization header).
Once the server URL is provided and connecting, click Next to go to next page.
This will list all entities published by the XData server. You can then select the ones you want to generate a listing page for. Unselected entities will not have an entry in the menu nor will have a listing page. Select the entities you want and click Next.
The final wizard page will ask you for a directory where the source code of the web application will be generated. Choose the output folder you want and click Finish. The application source code will be generated in the specified folder, and the project will be open in Delphi IDE.
You can now compile and run the application, and of course, modify it as you want. This is a easy and fast way to start coding with TMS Web Core and TMS XData backend. Here is a screenshot of the generated application running in the browser, using the settings above:
Extra Resources
There are additional quality resources about TMS Web Core and TMS XData available, in different formats (video training courses and books):
Online Training Course: Introduction to TMS Web Core
By Wagner Landgraf
https://courses.landgraf.dev/p/web-applications-with-delphi-tms-web-core
Book: TMS Software Hands-on With Delphi
(Cross-plataform Mult-tiered Database Applications: Web and Desktop
Clients, REST/JSON Server and Reporting, Book 1)
By Dr. Holger Flick
https://www.amazon.com/dp/B088BJLLWG/
Book: TMS WEB Core: Web Application Development with Delphi
By Dr. Holger Flick
https://www.amazon.com/dp/B086G6XDGW/
Book: TMS WEB Core: Webanwendungen mit Delphi entwickeln (German)
By Dr. Holger Flick
https://www.amazon.de/dp/1090700822/