Skip to main content

Record Iteration

FdcDataSet maintains a current record. Iteration moves that cursor through the active dataset view after filtering, sorting, and search are applied.

This is a programmatic record model, independent of whether the dataset is currently shown in a grid, form, or another data-aware control.

Move through records

dataSet.first();
dataSet.next();
dataSet.prior();
dataSet.last();

Move directly by visible record number:

dataSet.moveToRecord(25);

Record numbers are 1-based. The dataset uses -1 when there is no active record.

Inspect iteration state

final current = dataSet.recordNumber;
final count = dataSet.recordCount;
final beforeFirst = dataSet.bof;
final afterLast = dataSet.eof;

The current record belongs to the active view. A filter or search can therefore change both recordCount and the visible position of the current record without changing the underlying record identity.

Iterate with while (!eof)

For sequential programmatic processing, start at the first record and continue until the dataset reaches EOF:

dataSet.first();

while (!dataSet.eof) {
processCustomer(
id: dataSet['customer_id'],
name: dataSet['customer_name'],
);

dataSet.next();
}

The next() call must remain inside the loop. Omitting it leaves the cursor on the same record and creates an infinite loop.

For empty datasets, first() leaves the dataset at EOF, so the loop body is skipped naturally.

Batch control updates during iteration

Moving the current record can notify every data-aware control connected to the dataset. For a long programmatic loop, repeatedly rebuilding grids, forms, and editors is usually unnecessary.

Use disableControls() before the batch and always balance it with enableControls() in a try/finally block:

dataSet.disableControls();

try {
dataSet.first();

while (!dataSet.eof) {
processRecord(dataSet);
dataSet.next();
}
} finally {
dataSet.enableControls();
}

try/finally is important. If processRecord() throws, controls are still re-enabled. Without the finally block, the dataset could remain connected to controls while their notifications stay suspended.

What disabling controls actually does

disableControls() suspends ChangeNotifier delivery to data-aware controls. It does not pause or disable the dataset itself.

While controls are disabled, the dataset can still:

  • move the current record,
  • edit and post records,
  • validate values,
  • filter, sort, search, and page,
  • persist changes,
  • execute typed lifecycle callbacks.

When the outermost suspension ends, enableControls() delivers at most one deferred control notification instead of replaying every intermediate cursor change.

This makes control suspension useful for:

  • full-dataset calculations,
  • multi-record updates,
  • imports and normalization passes,
  • programmatic searches through records,
  • any batch workflow where intermediate UI states are not useful.

Restore the original current record

When the loop is only inspecting or processing records, preserve the user's position with restoreCurrentRecord: true:

dataSet.disableControls(restoreCurrentRecord: true);

try {
dataSet.first();

while (!dataSet.eof) {
inspectRecord(dataSet);
dataSet.next();
}
} finally {
dataSet.enableControls();
}

The dataset captures the current record for that suspension cycle and restores it immediately before the final control notification. The UI therefore sees the final restored position rather than every intermediate record visited by the loop.

Control suspension is nestable. Controls are re-enabled only after every disableControls() call has a matching enableControls() call. An unmatched enableControls() throws StateError, so the try/finally pattern should be treated as the standard usage pattern.

Read current values

Use field accessors or dataset indexing for current-record values:

final name = dataSet.fieldByName('name').value;
final status = dataSet['status'];

For writes, first enter edit or insert state:

dataSet.edit();
dataSet['status'] = 'active';
dataSet.post();

Bookmarks and cursor restoration

Use transient dataset bookmarks when a workflow needs to leave the current record temporarily and return later:

final bookmark = dataSet.bookmarks.create();

await openRelatedWorkflow();

if (bookmark != null) {
final restored = dataSet.bookmarks.restore(bookmark);
}

A bookmark stores record identity and its former visible position, but it is intentionally a runtime-only object.

Important semantics:

  • a bookmark is valid only for the dataset instance that created it,
  • it is not a serialization or layout-persistence format,
  • restore() returns true only when the original record still exists in the active view,
  • a bookmark from another dataset instance is rejected and returns false.

When the original record is no longer visible, optionally move to the nearest valid former position:

final restored = dataSet.bookmarks.restore(
bookmark,
fallbackToNearest: true,
);

If fallback movement occurs, restore() still returns false: the cursor moved to a useful nearby position, but the original bookmarked record was not restored.

Bookmarks are useful for:

  • refresh/requery workflows,
  • temporarily navigating to a related record,
  • modal edit workflows,
  • preserving user position around operations that can rebuild the active view.

Scroll callbacks

Use beforeScroll and afterScroll when application logic must react specifically to current-record movement:

final dataSet = FdcDataSet(
fields: fields,
afterScroll: (dataSet, previousRecordNumber, currentRecordNumber) {
// React to the new current record.
},
);

Iteration and editing

Record movement participates in the dataset edit lifecycle. Application code should not treat cursor changes as an unrelated UI concern: the dataset owns edit state, current-record state, and recordset transitions together.

For the full edit model, see Editing Lifecycle.