When developing web applications with TMS Web Core and XData, you'll often need to display lookup data in dropdown lists, radio button groups, or other selection controls. This data typically includes reference values like status codes, yes/no options, country lists, or category selections that don't change frequently and don't need to be stored in a database.
Virtual tables (also called memory tables or in-memory datasets) provide an elegant solution for this scenario. They allow you to create data-aware controls without the overhead of database queries, reducing server load and improving application responsiveness. This is particularly valuable for:
However, it's not immediately obvious how to implement virtual tables when using XData and TMS Web Core with Delphi. The standard VCL approaches don't translate directly to the web environment, and the XData client-side components require specific handling. This article provides a complete, self-contained solution that you can drop into your projects.
To use this implementation, you'll need:
unit Remote.Virtual;
interface
uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Modules, Data.DB,
WEBLib.DB, XData.Web.JsonDataset, XData.Web.Dataset;
type
TXDS = class (TXDataWebDataSet)
protected
FSrc : TWebDataSource;
procedure Add_Int_Field (const AField : string);
procedure Add_Chr_Field (const AField : string);
procedure Add_Str_Field (const AField : string;
const ALength : integer);
procedure Set_Data (const AData : string);
procedure Create_Datasource;
end;
TVirtual = class
private
class var xds_Status : TXDS;
class var xds_Yes_No : TXDS;
public
class function Status : TWebDataSource;
class function Yes_No : TWebDataSource;
end;
implementation
uses
WEB.Common;
{ TXDS }
procedure TXDS.Add_Chr_Field (const AField : string);
var
LField : TStringField;
begin
LField := TStringField.Create (Self);
LField.FieldName := AField;
LField.DataSet := Self;
end;
procedure TXDS.Add_Int_Field (const AField : string);
var
LField : TIntegerField;
begin
LField := TIntegerField.Create (Self);
LField.FieldName := AField;
LField.DataSet := Self;
end;
procedure TXDS.Add_Str_Field (const AField : string;
const ALength : integer);
var
LField : TStringField;
begin
LField := TStringField.Create (Self);
LField.Size := ALength;
LField.FieldName := AField;
LField.DataSet := Self;
end;
procedure TXDS.Set_Data (const AData : string);
begin
SetJsonData (TJSArray(TJSJson.Parse(AData)));
end;
procedure TXDS.Create_Datasource;
begin
FSrc := TWebDataSource.Create (Self);
FSrc.DataSet := Self;
if Assigned (FSrc)
then Console_Log ('Is Assigned');
end;
{ TVirtual }
class function TVirtual.Status : TWebDataSource;
const
LData = '[{"Status":"A", "Name":"Active"},'+
'{"Status":"D", "Name":"Deleted"},'+
'{"Status":"I", "Name":"Inactive"}]';
begin
if not assigned (xds_Status)
then begin
xds_Status := TXDS.Create (nil);
xds_Status.Add_Chr_Field ('Status');
xds_Status.Add_Str_Field ('Name', 20);
xds_Status.Set_Data (LData);
xds_Status.Create_Datasource;
xds_Status.Open;
end;
xds_Status.FSrc.Enabled := True;
Result := xds_Status.FSrc;
end;
class function TVirtual.Yes_No : TWebDataSource;
const
LData = '[{"Status":"N", "Name":"No"},'+
'{"Status":"Y", "Name":"Yes"}]';
var
LDTS : TWebDataSource;
begin
if not assigned (xds_Yes_No)
then begin
xds_Yes_No := TXDS.Create (nil);
xds_Yes_No.Add_Str_Field ('Status', 1);
xds_Yes_No.Add_Str_Field ('Name', 10);
xds_Yes_No.Set_Data (LData);
xds_Yes_No.Create_Datasource;
xds_Yes_No.Open;
end;
xds_Yes_No.FSrc.Enabled := True;
Result := xds_Yes_No.FSrc;
end;
end.
The TXDS class extends TXDataWebDataSet to encapsulate all the common functionality needed for creating virtual tables. By subclassing, we can:
Three methods handle different field types:
Each method creates the appropriate TField descendant, sets its FieldName property, and assigns it to the dataset. The fields are owned by the dataset (Self), ensuring proper memory management.
The Set_Data method is crucial - it parses a JSON array string and loads it into the dataset using SetJsonData. The JSON structure must be an array of objects where each object represents a row, and each property represents a field value:
[
{"FieldName1": "Value1", "FieldName2": "Value2"},
{"FieldName1": "Value3", "FieldName2": "Value4"}
]
Important: The JSON must be strictly formatted - no trailing commas, proper quotes, and valid syntax. Use a JSON validator if you encounter parsing errors.
The Create_Datasource method creates a TWebDataSource and connects it to the dataset. The datasource is stored in the FSrc field and is owned by the dataset, ensuring it's freed when the dataset is destroyed. The Console_Log call is useful for debugging to confirm the datasource was created successfully.
The TVirtual class uses class variables (class var) to store the dataset instances. This means:
Both Status and Yes_No functions use lazy initialization:
if not assigned (xds_Status)
then begin
// Create and configure the dataset
end;
This means the virtual table is only created the first time it's requested, not when your application starts. This reduces startup time and memory usage.
The functions return a TWebDataSource rather than the dataset itself. This is because data-aware controls like TWebDBLookupComboBox expect a datasource, not a dataset. The FSrc.Enabled := True line ensures the datasource is active before returning it.
The most common use case is populating dropdown lists in data entry forms. Here's how to use the virtual tables with TWebDBLookupComboBox controls:
{ Both edStatus and edIs_Helicopter are "TWebDBLookupComboBoxes".
I have populated the "KeyField" and "ListField" in the object inspector
For edStatus (as an example):
- KeyField: 'Status' (the field that stores the key value in your main dataset)
- ListField: 'Name' (the field that displays to the user)
- ListSource: (assigned at runtime below)
This allows me to save 'A', 'I', and 'D' in the Database
But the user sees 'Active', 'Inactive' and 'Deleted' in the dropdown.
}
procedure TFormExample.Prepare_For_Edit_And_Insert;
begin
edStatus.ListSource := TVirtual.Status;
edIs_Helicopter.ListSource := TVirtual.Yes_No;
end;
procedure FormExample_XDataWebDataSet_AfterEdit (DataSet : TDataSet);
begin
Prepare_For_Edit_And_Insert;
end;
procedure FormExample_XDataWebDataSet_AfterInsert (DataSet : TDataSet);
begin
Prepare_For_Edit_And_Insert;
end;
Setting the ListSource in the AfterEdit and AfterInsert events ensures the lookup data is available whenever the user begins editing. This is important because:
For simpler scenarios, you can set the ListSource once when the form is created:
procedure TFormExample.WebFormCreate(Sender: TObject);
begin
edStatus.ListSource := TVirtual.Status;
edIs_Helicopter.ListSource := TVirtual.Yes_No;
end;
This approach works well when the virtual tables don't change and don't depend on any runtime conditions.
To add additional virtual tables, follow this pattern:
// Add to TVirtual class private section:
class var xds_Priority : TXDS;
// Add to TVirtual class public section:
class function Priority : TWebDataSource;
// Implementation:
class function TVirtual.Priority : TWebDataSource;
const
LData = '[{"Code":"L", "Name":"Low"},'+
'{"Code":"M", "Name":"Medium"},'+
'{"Code":"H", "Name":"High"},'+
'{"Code":"U", "Name":"Urgent"}]';
begin
if not assigned (xds_Priority)
then begin
xds_Priority := TXDS.Create (nil);
xds_Priority.Add_Str_Field ('Code', 1);
xds_Priority.Add_Str_Field ('Name', 10);
xds_Priority.Set_Data (LData);
xds_Priority.Create_Datasource;
xds_Priority.Open;
end;
xds_Priority.FSrc.Enabled := True;
Result := xds_Priority.FSrc;
end;
If you see errors when the virtual table is created, check your JSON:
If your dropdown appears empty:
The implementation uses class variables which are never freed during the application lifecycle. This is intentional - the virtual tables exist for the entire session. However, if you need to explicitly free them (rare), you'd need to add cleanup code to your main form's OnDestroy event.
Use virtual tables for:
Use database tables for:
For more flexibility, you can load JSON data from external files or API endpoints:
procedure TXDS.Load_From_URL(const AURL: string);
begin
// Use TWebHttpRequest to fetch JSON from URL
// Parse and call SetJsonData with the result
// This allows updating virtual table data without recompiling
end;
This approach is useful when lookup data needs to be updated occasionally without deploying new code.
Please donate if helpful