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';
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;
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
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 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 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.
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;
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');
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;
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
|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:
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;
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
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;
- 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.
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:
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.
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;
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;
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:
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.
|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.|
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:
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.
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:
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
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
Book: TMS WEB Core: Web Application Development with Delphi
By Dr. Holger Flick
Book: TMS WEB Core: Webanwendungen mit Delphi entwickeln (German)
By Dr. Holger Flick