Pacific Blue Software Logo

How to implement a virtual table on TMS Webcore XData Client?

How to Create a virtual table on TMS Webcore XData Client?

Introduction

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.

Prerequisites

To use this implementation, you'll need:

Implementation


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.
  

Understanding the Implementation

Why Subclass TXDataWebDataSet?

The TXDS class extends TXDataWebDataSet to encapsulate all the common functionality needed for creating virtual tables. By subclassing, we can:

Field Creation Methods

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.

JSON Data Handling

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.

DataSource Management

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.

Class Variables vs Instance Variables

The TVirtual class uses class variables (class var) to store the dataset instances. This means:

Lazy Initialization Pattern

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.

Returning the DataSource

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.

Detailed Explanation

  1. JSON Strictness - The JSON data structure is very strict. Don't have stray commas, missing quotes, or invalid syntax. Use a JSON validator tool if you encounter parsing errors. Common mistakes include trailing commas after the last element or using single quotes instead of double quotes.
  2. Multiple Tables - The sample shows two independent virtual tables (Status and Yes_No). You can add as many as needed by following the same pattern. Each table is completely independent with its own fields and data.
  3. Code Organization - The TXDS subclass holds common functionality that would otherwise be duplicated. This makes the code more maintainable and follows the DRY (Don't Repeat Yourself) principle.
  4. Class-based Design - Rather than using global procedures and functions, the static methods are organized within the TVirtual class. This provides better namespacing, prevents naming conflicts, and makes the code more maintainable in larger projects.
  5. DataSource Return - The functions return a TWebDataSource because this is what data-aware controls expect. This design allows for direct assignment to control properties like ListSource without additional wrapper code.

Practical Usage Examples

Basic Lookup ComboBox

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;
  

Why Set ListSource in AfterEdit and AfterInsert?

Setting the ListSource in the AfterEdit and AfterInsert events ensures the lookup data is available whenever the user begins editing. This is important because:

Alternative: Setting ListSource in FormCreate

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.

Adding More Virtual Tables

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;

Common Pitfalls and Troubleshooting

JSON Parsing Errors

If you see errors when the virtual table is created, check your JSON:

Empty Dropdowns

If your dropdown appears empty:

Memory Leaks

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.

Performance Considerations

When to Use Virtual Tables vs Database Tables

Use virtual tables for:

Use database tables for:

Advanced: Loading from External JSON

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.

Related Topics


Sample Code to Create a virtual table on TMS Webcore XData Client?


Back to Articles for Developers
Back to Articles on Delphi
How to Handle Nil Pointers in TMS WebCore
Convert Web colour to Delphi (Windows)

If you found this useful, then please consider making a donation.

paypal
QR Code for donation Please donate if helpful