Skip to main content
Table of Contents

Headless data layer

Use the data layer independently — sort, filter, group, and export without any visual component on screen. Useful for background processing, batch exports, and unit-testable data pipelines.

Overview

TTMSFNCDataGridData is the layer that stores cell values and owns all data operations: sorting, filtering, grouping, and CSV export. Because it has no dependency on drawing or controls, you can instantiate it anywhere — including a background thread, a service, or a console application — and run full data operations without displaying anything.

The same API works on both the headless layer and on a live grid: Filter.Add, ApplyFilter, Sort, Group, Strings[], Floats[], Ints[], SaveToCSVData are all on TTMSFNCDataGridData and therefore available on TTMSFNCDataGrid too.

Quick example

Load 500 JSON records, filter by category, sort by price, and log the top five:

Headless quick examplePascalC++
uses
  System.SysUtils, FMX.TMSFNCDataGridData;

procedure TMyForm.LoadRecords;
begin
  FData.ClearData;
  FData.FixedRowCount       := 1;
  FData.Options.IO.StartRow := 1; // row 0 receives JSON key names as headers

  FData.LoadFromJSONData(
    JSONPath,
    '', '',
    ['id', 'name', 'category', 'price', 'stock', 'is_organic', 'is_vegan']);

  Log(Format('Loaded %d records', [FData.RowCount - FData.FixedRowCount]));
end;
void __fastcall TMyForm::LoadRecords()
{
  FData->ClearData();
  FData->FixedRowCount       = 1;
  FData->Options->IO->StartRow = 1; // row 0 receives JSON key names as headers

  DynamicArray<System::UnicodeString> Names;
  Names.Length = 7;
  Names[0] = "id";
  Names[1] = "name";
  Names[2] = "category";
  Names[3] = "price";
  Names[4] = "stock";
  Names[5] = "is_organic";
  Names[6] = "is_vegan";
  FData->LoadFromJSONData(JSONPath, "", "", Names);

  Log(Format("Loaded %d records", ARRAYOFCONST((FData->RowCount - FData->FixedRowCount))));
}

Step by step

1. Create and destroy

Create and destroy a headless data layerPascalC++
type
  TMyForm = class(TForm)
  private
    FData: TTMSFNCDataGridData;
  public
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  end;

procedure TMyForm.FormCreate(Sender: TObject);
begin
  FData := TTMSFNCDataGridData.Create(nil);   // the constructor takes the host object
end;

procedure TMyForm.FormDestroy(Sender: TObject);
begin
  FData.Free;
end;
class TMyForm : public TForm
{
private:
  TTMSFNCDataGridData *FData;
public:
  __fastcall TMyForm(TComponent *Owner) : TForm(Owner) {}
  void __fastcall FormCreate(TObject *Sender);
  void __fastcall FormDestroy(TObject *Sender);
};

void __fastcall TMyForm::FormCreate(TObject *Sender)
{
  FData = new TTMSFNCDataGridData(nullptr);   // the constructor takes the host object
}

void __fastcall TMyForm::FormDestroy(TObject *Sender)
{
  delete FData;
}

TTMSFNCDataGridData is an ordinary object — it allocates on the heap and must be freed explicitly.

2. Load data

The data layer supports the same load methods as the grid:

Load data into the headless layerPascalC++
uses
  FMX.TMSFNCDataGridData;

procedure TMyForm.LoadData(const FileName: string);
begin
  // From JSON (most common for REST payloads)
  FData.LoadFromJSONData(FileName, '', '',
    ['id', 'name', 'category', 'price', 'stock']);

  // From CSV
  FData.Options.IO.Delimiter := ',';
  FData.LoadFromCSVData('products.csv');

  // Manually
  FData.ColumnCount   := 3;
  FData.FixedRowCount := 1;
  FData.RowCount      := 4;
  FData.Cells[0, 0] := 'ID';
  FData.Cells[1, 0] := 'Name';
  FData.Cells[2, 0] := 'Price';
  FData.Cells[0, 1] := 1;  FData.Cells[1, 1] := 'Widget A';  FData.Cells[2, 1] := 9.99;
  FData.Cells[0, 2] := 2;  FData.Cells[1, 2] := 'Widget B';  FData.Cells[2, 2] := 14.50;
  FData.Cells[0, 3] := 3;  FData.Cells[1, 3] := 'Widget C';  FData.Cells[2, 3] := 4.25;
end;
void __fastcall TMyForm::LoadData(const System::UnicodeString FileName)
{
  // From JSON (most common for REST payloads)
  DynamicArray<System::UnicodeString> Names;
  Names.Length = 5;
  Names[0] = "id";
  Names[1] = "name";
  Names[2] = "category";
  Names[3] = "price";
  Names[4] = "stock";
  FData->LoadFromJSONData(FileName, "", "", Names);

  // From CSV
  FData->Options->IO->Delimiter = L',';
  FData->LoadFromCSVData("products.csv");

  // Manually
  FData->ColumnCount   = 3;
  FData->FixedRowCount = 1;
  FData->RowCount      = 4;
  FData->Strings[0][0] = "ID";
  FData->Strings[1][0] = "Name";
  FData->Strings[2][0] = "Price";
  FData->Ints[0][1] = 1;  FData->Strings[1][1] = "Widget A";  FData->Floats[2][1] = 9.99;
  FData->Ints[0][2] = 2;  FData->Strings[1][2] = "Widget B";  FData->Floats[2][2] = 14.50;
  FData->Ints[0][3] = 3;  FData->Strings[1][3] = "Widget C";  FData->Floats[2][3] = 4.25;
}

Set FData.Options.IO.StartRow := 1 before loading JSON so that row 0 receives the JSON key names as column headers automatically.

3. Filter

Filter the headless layerPascalC++
uses
  System.SysUtils, FMX.TMSFNCDataGridData;

procedure TMyForm.FilterData;
var
  R, Count: Integer;
begin
  FData.Filter.Clear;
  FData.Filter.Add(COL_CATEGORY, gftEqual, 'Meat');
  FData.ApplyFilter;

  // After ApplyFilter, iterate visible rows with IsRowDisplayed
  Count := 0;
  for R := FData.FixedRowCount to FData.RowCount - 1 do
    if FData.IsRowDisplayed(R) then
      Inc(Count);
  Log(Format('%d matching rows', [Count]));

  // Remove all filters and show every row again
  FData.RemoveFilter;
end;
void __fastcall TMyForm::FilterData()
{
  FData->Filter->Clear();
  FData->Filter->Add(COL_CATEGORY, gftEqual, TValue::From<System::UnicodeString>("Meat"));
  FData->ApplyFilter();

  // After ApplyFilter, iterate visible rows with IsRowDisplayed
  int Count = 0;
  for (int R = FData->FixedRowCount; R < FData->RowCount; R++)
    if (FData->IsRowDisplayed(R))
      Count++;
  Log(Format("%d matching rows", ARRAYOFCONST((Count))));

  // Remove all filters and show every row again
  FData->RemoveFilter();
}

4. Sort

Sort the headless layerPascalC++
uses
  FMX.TMSFNCDataGridData;

procedure TMyForm.SortData;
begin
  FData.Sort(COL_PRICE, gsdDescending);
end;
void __fastcall TMyForm::SortData()
{
  FData->Sort(COL_PRICE, gsdDescending);
}

Sort and filter interact: sorting re-orders only the displayed rows; hidden rows stay hidden.

5. Group with aggregations

Group and aggregate the headless layerPascalC++
uses
  System.SysUtils, FMX.TMSFNCDataGridData;

procedure TMyForm.GroupData;
var
  R: Integer;
begin
  FData.Group(COL_CATEGORY);
  FData.GroupCount(COL_CATEGORY); // count column
  FData.GroupSum(COL_PRICE);      // sum column

  // Walk the result to inspect group rows and their summaries
  for R := FData.FixedRowCount to FData.RowCount - 1 do
  begin
    if FData.IsRowNode(R) then
      Log(FData.Strings[FData.FixedColumnCount, R]) // group header text
    else if FData.IsRowSummary(R) then
      Log(Format('  count=%.0f   sum=$%.2f',
        [FData.Floats[COL_CATEGORY, R], FData.Floats[COL_PRICE, R]]));
  end;

  FData.Ungroup;
end;
void __fastcall TMyForm::GroupData()
{
  FData->Group(COL_CATEGORY);
  FData->GroupCount(COL_CATEGORY); // count column
  FData->GroupSum(COL_PRICE);      // sum column

  // Walk the result to inspect group rows and their summaries
  for (int R = FData->FixedRowCount; R < FData->RowCount; R++)
  {
    if (FData->IsRowNode(R))
      Log(FData->Strings[FData->FixedColumnCount][R]); // group header text
    else if (FData->IsRowSummary(R))
      Log(Format("  count=%.0f   sum=$%.2f",
        ARRAYOFCONST(((long double)FData->Floats[COL_CATEGORY][R], (long double)FData->Floats[COL_PRICE][R]))));
  }

  FData->Ungroup();
}

6. Export

Export the headless layer to CSVPascalC++
uses
  FMX.TMSFNCDataGridData;

procedure TMyForm.ExportData;
begin
  FData.Options.IO.Delimiter := ',';
  FData.SaveToCSVData('output.csv');
end;
void __fastcall TMyForm::ExportData()
{
  FData->Options->IO->Delimiter = L',';
  FData->SaveToCSVData("output.csv");
}

7. Read typed values

Use typed accessors for performance in tight loops:

Read typed valuesPascalC++
uses
  FMX.TMSFNCDataGridData;

procedure TMyForm.ReadTypedValues(Row: Integer);
var
  Name: string;
  Price: Double;
  Stock: Integer;
begin
  Name  := FData.Strings[COL_NAME,  Row];
  Price := FData.Floats [COL_PRICE, Row];
  Stock := FData.Ints   [COL_STOCK, Row];
end;
void __fastcall TMyForm::ReadTypedValues(int Row)
{
  System::UnicodeString Name = FData->Strings[COL_NAME][Row];
  double Price = FData->Floats[COL_PRICE][Row];
  int Stock = FData->Ints[COL_STOCK][Row];
}

Connecting the headless layer to a visual grid

If you later want to display the processed data in a TTMSFNCDataGrid, you can copy it row by row or simply re-load the same source into the grid:

Load the same source into a visual gridPascalC++
uses
  FMX.TMSFNCDataGrid;

procedure TMyForm.ShowInGrid(const FileName: string);
begin
  Grid.LoadFromJSONData(FileName, '', '', ['id', 'name', 'price']);
end;
void __fastcall TMyForm::ShowInGrid(const System::UnicodeString FileName)
{
  DynamicArray<System::UnicodeString> Names;
  Names.Length = 3;
  Names[0] = "id";
  Names[1] = "name";
  Names[2] = "price";
  Grid->LoadFromJSONData(FileName, "", "", Names);
}

For a shared-source pattern, use TTMSFNCDataGridDatabaseAdapter against a common TDataSet. The data layer is best used for scenarios where you need data operations without any UI — for example, generating a filtered CSV in a background thread or computing statistics at startup.

Combining load, filter, sort and export

The headless data layer shines when you chain its operations into one pipeline, for example to produce a filtered, sorted CSV in a background task. Use it whenever the result goes to a file or a report instead of the screen. The example loads a small table by hand, filters on a category, totals the displayed rows with IsRowDisplayed, sorts by price, and saves the result with SaveToCSVData. The object is created with an owner, used inside try..finally and freed explicitly. Totalling happens before the sort, because the filter decides which rows are displayed, and the sort only reorders them.

Filter, sort and export in one headless pipelinePascalC++
procedure TForm1.RunHeadlessPipeline;
var
  Data: TTMSFNCDataGridData;
  R: Integer;
  Total: Double;
begin
  Data := TTMSFNCDataGridData.Create(Self);
  try
    // Load: build a small table by hand (one header row).
    Data.ColumnCount := 3;
    Data.FixedRowCount := 1;
    Data.RowCount := 5;
    Data.Strings[0, 0] := 'Name';
    Data.Strings[1, 0] := 'Category';
    Data.Strings[2, 0] := 'Price';
    Data.Strings[0, 1] := 'Steak';   Data.Strings[1, 1] := 'Meat';  Data.Floats[2, 1] := 12.5;
    Data.Strings[0, 2] := 'Apple';   Data.Strings[1, 2] := 'Fruit'; Data.Floats[2, 2] := 0.8;
    Data.Strings[0, 3] := 'Sausage'; Data.Strings[1, 3] := 'Meat';  Data.Floats[2, 3] := 4.2;
    Data.Strings[0, 4] := 'Ham';     Data.Strings[1, 4] := 'Meat';  Data.Floats[2, 4] := 7.9;

    // Filter: keep the Meat rows and total them with IsRowDisplayed.
    Data.Filter.Clear;
    Data.Filter.Add(1, gftEqual, 'Meat');
    Data.ApplyFilter;

    Total := 0;
    for R := Data.FixedRowCount to Data.RowCount - 1 do
      if Data.IsRowDisplayed(R) then
        Total := Total + Data.Floats[2, R];
    Caption := Format('Meat total: %.2f', [Total]);

    // Sort the filtered rows by price and export them.
    Data.Sort(2, gsdDescending);
    Data.Options.IO.Delimiter := ',';
    Data.SaveToCSVData('meat-by-price.csv');
  finally
    Data.Free;
  end;
end;
void __fastcall TForm1::RunHeadlessPipeline()
{
  TTMSFNCDataGridData *Data = new TTMSFNCDataGridData(this);
  try
  {
    // Load: build a small table by hand (one header row).
    Data->ColumnCount = 3;
    Data->FixedRowCount = 1;
    Data->RowCount = 5;
    Data->Strings[0][0] = "Name";
    Data->Strings[1][0] = "Category";
    Data->Strings[2][0] = "Price";
    Data->Strings[0][1] = "Steak";   Data->Strings[1][1] = "Meat";  Data->Floats[2][1] = 12.5;
    Data->Strings[0][2] = "Apple";   Data->Strings[1][2] = "Fruit"; Data->Floats[2][2] = 0.8;
    Data->Strings[0][3] = "Sausage"; Data->Strings[1][3] = "Meat";  Data->Floats[2][3] = 4.2;
    Data->Strings[0][4] = "Ham";     Data->Strings[1][4] = "Meat";  Data->Floats[2][4] = 7.9;

    // Filter: keep the Meat rows and total them with IsRowDisplayed.
    Data->Filter->Clear();
    Data->Filter->Add(1, gftEqual, TValue::From<System::UnicodeString>("Meat"));
    Data->ApplyFilter();

    double Total = 0;
    for (int R = Data->FixedRowCount; R < Data->RowCount; R++)
      if (Data->IsRowDisplayed(R))
        Total = Total + Data->Floats[2][R];
    Caption = Format("Meat total: %.2f", ARRAYOFCONST(((long double)Total)));

    // Sort the filtered rows by price and export them.
    Data->Sort(2, gsdDescending);
    Data->Options->IO->Delimiter = ',';
    Data->SaveToCSVData("meat-by-price.csv");
  }
  __finally
  {
    delete Data;
  }
}
  • TTMSFNCDataGridData
  • FData.LoadFromJSONData / LoadFromCSVData
  • FData.Filter.Add(AColumn, AFilterType, ACondition) — typed filter overload
  • FData.ApplyFilter / RemoveFilter
  • FData.IsRowDisplayed(ARow) — True if the row is visible after filter
  • FData.IsRowNode(ARow) — True if the row is a group header
  • FData.IsRowSummary(ARow) — True if the row is a group summary
  • FData.Sort(AColumn, ADirection)
  • FData.Group(AColumn) / Ungroup
  • FData.GroupSum / GroupCount / GroupAverage / GroupMin / GroupMax
  • FData.SaveToCSVData(AFileName) — delimiter set via Options.IO.Delimiter
  • FData.Strings[col, row] / Floats / Ints / Booleans — typed accessors
  • FData.FixedRowCount / ColumnCount / RowCount
  • FData.Options.IO.StartRow — set to 1 so JSON keys land in row 0 as headers

See also