How to Page and Sort a Cached Dataset in Pascal

A paged view over an in-memory dataset has to filter and sort the whole dataset before it can cut a page. Repeating that load, filter and sort for every Next click is wasted work when neither the data nor the query has changed.

TCustomDataSource<T> in Sphere 10 Pascal Framework implements that query path once, and can keep both the loaded dataset and the filtered, sorted view between page requests. This article shows the three methods a data source overrides, how a caller describes a filter and a sort, what the snapshot cache retains, and how to invalidate it.

Sorted records and bounded pages drawn from one cached dataset.
Sorted records and bounded pages drawn from one cached dataset.
💡 In short. Implement a TCustomDataSource, describe filters and sorts with TColumnFilter, and use its snapshot cache to page a filtered, sorted dataset without reloading it.

1. Describe the query to the data source

A caller asks the data source for a page by describing a query. A query is an array of TColumnFilter records. Each one names a column and can carry a filter, a sort direction or both. The sample asks for records whose Value field is below 25, in ascending order of that same field.

Pascal
    Source.SnapshotCacheEnabled := True;
    Provider := Source;
    SetLength(Filters, 1);
    Filters[0] := Default(TColumnFilter);
    Filters[0].ColumnName := 'Value';
    Filters[0].Filter := vgfNumericLT;
    SetLength(Filters[0].Values, 1);
    Filters[0].Values[0] := 25;
    Filters[0].Sort := sdAscending;

The first statement switches on the snapshot cache, which a later section covers. The page request combines the filters with a page index, a page size and the rule that joins several filters, which is foAnd or foOr.

Pascal
      Params := TPageFetchParams.Create(PageIndex, 10, Filters, foAnd);
      Page := Provider.FetchPage(Params, Table);

Table.Rows holds the page, and each row exposes its fields by name, as in Table.Rows[I].Value. The returned TPageFetchResult reports PageIndex, PageCount and TotalDataCount for the whole filtered result. A page index beyond the last page is clipped to the last page, and a page size of zero or less returns everything as one page.

2. How a page is produced

A consumer such as TVisualGrid calls IDataSource.FetchPage with a TPageFetchParams record and receives a TDataTable of rows. TCustomDataSource<T> implements that call in four steps. It obtains every record, applies the filters, applies the sort and then converts only the requested page into rows.

With the snapshot cache switched off, all four steps run on every call. With it switched on, the first two results are retained.

The diagram shows the cached case. FetchAll runs for the first request and again only after InvalidateSnapshot.

3. Implement the data source

A data source derives from TCustomDataSource<T>, where T is the record or class it serves, and overrides three methods.

Pascal
  TItemSource = class(TCustomDataSource<TItem>)
  public
    FetchCount: Integer;
    ValueOffset: Integer;
  protected
    function GetColumns: TDataColumns; override;
    procedure FetchAll(const AContainer: TList<TItem>); override;
    function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: TItem; const ABindingName: String): Variant; override;
  end;
Override Responsibility
GetColumns Names the fields that rows expose. The second argument of TDataColumn.From marks a key column.
FetchAll Adds every record to the supplied list. This is the only place that touches the real source.
GetItemField Returns one named field of one record as a Variant. Filtering, sorting and row conversion all read values through it.
Pascal
function TItemSource.GetColumns: TDataColumns;
begin
  Result := TDataColumns.Create(TDataColumn.From('Id', True), TDataColumn.From('Name'), TDataColumn.From('Value'));
end;

Because every comparison goes through GetItemField, the base class can filter and sort any record type without knowing its layout.

4. What the snapshot cache retains

Setting SnapshotCacheEnabled to True makes the data source keep two things, which are the raw dataset returned by FetchAll and one filtered and sorted view of it. Each later request is compared with the query that built the view.

Request Work performed
Another page of the same query Converts the visible rows only.
A different filter, sort or operand Rebuilds the view from the retained raw dataset. FetchAll is not called.
First request after InvalidateSnapshot Calls FetchAll and rebuilds the view.
Any request with the cache disabled Calls FetchAll, filters and sorts every time.

The sample counts calls to FetchAll. It reads three pages of ten, ten and four rows from a single fetch, and then runs a different filter without fetching again.

5. Invalidate the snapshot when the data changes

The cache cannot know when the underlying data has changed, so the application tells it. InvalidateSnapshot marks both retained results as out of date, and the next FetchPage loads them again.

Pascal
    Source.ValueOffset := 1;
    Source.InvalidateSnapshot;
    Page := Provider.FetchPage(Params, Table);

InvalidateSnapshot only records that a rebuild is due. It does not wait for a fetch that is running on another thread, which may finish with the older snapshot. Changing SnapshotCacheEnabled invalidates in the same way.

6. Limits

â„šī¸ The snapshot cache is opt-in and is intended for immutable value snapshots, such as records.
âš ī¸ Keep these limits in mind.
  • The first request still loads the complete dataset through FetchAll. The cache avoids repeating that work and does not page at the source. A large or remote dataset needs its own IDataSource implementation.
  • When T is a class, the cached items are borrowed references rather than copies. The objects must stay alive for as long as the snapshot and any running fetch can use them.
  • The cache holds one query view. Alternating between two different queries rebuilds the view each time, although the raw dataset is reused.

The data-source unit contains TCustomDataSource<T> and the filter types. The adaptive grid article binds the same kind of data source to TVisualGrid.

7. Get the Framework and run the example

Get Sphere 10 Pascal Framework and follow its package instructions . Use the units directly, or build the optional Lazarus or Delphi runtime package.

The sample was compiled and run on Windows with Free Pascal 3.2.0, against source revision 2cc6558 . Its Build.ps1 accepts the Framework, compiler, Lazarus and output paths. The program checks the order and size of each page and prints the number of FetchAll calls.

Plain text
Filtered rows=24; pages=10,10,4; FetchAll calls=1
New filter: rows=49; FetchAll calls=1
After InvalidateSnapshot: rows=48; FetchAll calls=2

Download the complete runnable sample.

sample.zip
Download the complete worked example and build instructions (ZIP).

For the broader Sphere 10 development toolkit, see the Sphere10 Framework overview .

Author: Herman Schoenfeld