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:
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
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:
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 := 1before loading JSON so that row 0 receives the JSON key names as column headers automatically.
3. Filter
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
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
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
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:
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:
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.
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;
}
}
Related API
TTMSFNCDataGridDataFData.LoadFromJSONData/LoadFromCSVDataFData.Filter.Add(AColumn, AFilterType, ACondition)— typed filter overloadFData.ApplyFilter/RemoveFilterFData.IsRowDisplayed(ARow)— True if the row is visible after filterFData.IsRowNode(ARow)— True if the row is a group headerFData.IsRowSummary(ARow)— True if the row is a group summaryFData.Sort(AColumn, ADirection)FData.Group(AColumn)/UngroupFData.GroupSum/GroupCount/GroupAverage/GroupMin/GroupMaxFData.SaveToCSVData(AFileName)— delimiter set viaOptions.IO.DelimiterFData.Strings[col, row]/Floats/Ints/Booleans— typed accessorsFData.FixedRowCount/ColumnCount/RowCountFData.Options.IO.StartRow— set to 1 so JSON keys land in row 0 as headers
See also
- Architecture — where the data layer fits in the three-layer design.
- Data, formatting & conversion — how values are stored.
- Filtering — full filtering reference.
- Grouping — grouping and aggregation reference.
- Import (JSON) — JSON structure mapping options.