Adding fields to a TDataset in code

The Delphi IDE allows you to add fields to a TDataset (descendant e.g. TTable, TQuery, TAdoTable etc.) by right clicking on the component and selecting “Add Field” or “New Field”. For a particular project I didn’t want to do that because I kept changing the query for which I want to add the fields. But since there was one calculated field I had to add fields to the dataset otherwise the OnCalcFields event wouldn’t be called. So I ended up adding the fields in code.

Basically that’s easy, you just create a TField descendant (e.g. TStringField,TAutoIncField, TDateTimeField etc.) set the field name and dataset and that’s it. But there are some pitfalls:

var
  fld: TField;
  ds: TDataset;
begin
  // init Dataset
  // ...
  // add fields
  fld := TWideStringField.Create(ds);
  fld.Name := '';
  fld.FieldName := 'TE_Employee';
  ds.Fields.Add(fld);

While this looks fine, it will not work. You will get the error “Field does not have a dataset” (or similar, I don’t remember the exact error message). The reason is that adding the field to the Dataset’s Fields collection does not automatically tell the field to which dataset it belongs. For that you need to assign the field’s Dataset property.

var
  fld: TField;
  ds: TDataset;
begin
  // init Dataset
  // ...
  // add fields
  fld := TWideStringField.Create(ds);
  fld.Name := '';
  fld.FieldName := 'TE_Employee';
  fld.Dataset := ds;
  ds.Fields.Add(fld);

If you write it like this, it will seem to work, but you will get an access violation when the dataset is being destroyed. Can you spot why?

I had to trace into the RTL sources to find the problem: The field was added to the Fields collection twice, because setting its Dataset property also adds it to the Fields collection. So, when the field’s collection got freed, the Field was freed twice, resulting in the AV.

So the correct and working code is this:

var
  fld: TField;
  ds: TDataset;
begin
  // init Dataset
  // ...
  // add fields
  fld := TWideStringField.Create(ds);
  fld.Name := '';
  fld.FieldName := 'TE_Employee';
  fld.Dataset := ds; // this automatically adds the field to the Dataset's Fields collection.

You might be wondering about the line

  fld.Name := '';

I set the name of all components created in code to an empty string to avoid name collisions with any existing components or even a different instance of the component created by the same code. “A component with the name Edit1 already exists.” has bitten me too often.

Since I also used a TDbGrid to display the dataset, I also added Columns to the DBGrid to set the caption and column width. And since I am lazy (as any good programmer should be), I moved most of that code into a (sub-) procedure to make the code more readable (actually I did it so I had less to type, but the result is the same):

var
  TheDataset: TDataset;
  TheGrid: TDbGrid;

procedure TMyForm.InitDbGrid;

  procedure AddField(_fld: TField; const _Fieldname: string; const _Caption: string; _Width: Integer);
  var
    Col: TColumn;
  begin
    _fld.FieldName := _Fieldname;
    _fld.ReadOnly := True;
    _fld.Dataset := TheDataset;
    Col := TheGrid.Columns.Add;
    Col.Expanded := False;
    Col.FieldName := _Fieldname;
    Col.Title.Caption := _Caption;
    Col.Width := _Width;
  end;

begin
  AddField(TAutoIncField.Create(nil), 'SeNr', _('Number'), 50);
  AddField(TDateTimeField.Create(nil), 'TeArbeitstag', _('Date'), 70);
  AddField(TWideStringField.Create(nil), 'SeVon', _('Start'), 40);
  AddField(TWideStringField.Create(nil), 'SeBis', _('End'), 40);
  fld := TWideStringField.Create(nil);
  fld.FieldKind := fkCalculated;
  AddField(fld, 'Stunden', _('Hours'), 50);
  AddField(TWideStringField.Create(nil), 'PROJ_IDENT', _('Project'), 50);
  AddField(TWideStringField.Create(nil), 'SSchluessel', _('Activity'), 50);
  AddField(TWideStringField.Create(nil), 'SeBeschreibung', _('Description'), 200);
  ResizeGridColumns;
end;

And just to show why this code is actually useful, here are the ResizeGridColumns method and TheDatasetCalcFields and FormResize events.

procedure TMyForm.ResizeGridColumns;
var
  cnt: Integer;
  TotalWidth: Integer;
  i: Integer;
begin
  TotalWidth := 0;
  cnt := TheGrid.Columns.Count;
  for i := 0 to cnt - 2 do begin
    TotalWidth := TotalWidth + TheGrid.Columns[i].Width;
  end;
  TheGrid.Columns[cnt - 1].Width := TheGrid.ClientWidth - TotalWidth - 50;
end;

procedure TMyForm.FormResize(Sender: TObject);
begin
  ResizeGridColumns;
end;

procedure TMyForm.TheDatasetCalcFields(Dataset: TDataSet);
var
  Von: TdzNullableTime;
  Bis: TdzNullableTime;
begin
  Von.AssignVariant(Dataset['seVon']);
  Bis.AssignVariant(Dataset['seBis']);
  Dataset.FieldByName('Stunden').Value := (Bis - Von).ToHourStr(2);
end;

(TdzNullableTime is an extended record type from my dzlib library which is available from sourceforge if you are interested. It is declared in u_dzNullableTime.pas.