Datasets without databases

By: Marco Canty

Abstract: How do you access non-relational data (such as a disk directory) with data-aware controls? Rely on a key element of Delphi: the TDataSet class. By Marco Canty. Link to code corrected June 7, 2000.

Datasets without databases

How do you access non-relational data (such as a disk directory) with data-aware controls? Rely on a key element of Delphi: the TDataSet class.

By Marco Canty

A favorite subject, of late, among Delphi programmers is the merit of ADO.  During the last couple of weeks, friends have been praising the power of  ADO to access data through OLE-DB providers you can write. For example, you can access mail folders or the Windows 2000 Active Directory using the ADSI API. This struck me, not because I've decided to write an OLE-DB provider in Delphi, but because I've realized that Delphi already has a much more powerful tool than ADO offers: the TDataSet class of the VCL.

Developers use BDE or ADO as database engines to abstract access to data in different formats and from database servers. Although both approaches are used in Delphi, the real abstraction Delphi provides for data access is the TDataSet class. This class provides a high-level interface to the data used by various data-aware components. Since Delphi 3, this structure has really been opening up. Borland has begun to introduce TDataSet-derived classes (including TClientDataSet, TADODataSet, and TIBDataSet for InterBase support), as have a number of third-party vendors.

Data diversions

The custom dataset I wrote is capable of reading file data and allowing a user to display it in a data-aware component, as shown in the following figure:

You might have heard that writing a custom dataset in Delphi is a lot of work. It generally is, mainly because documentation on the topic has always been sparse. The three books I know of that address custom dataset writing are Using Delphi 3, Delphi Developer's Guide, and my Delphi Developer's Handbook. Also, The Delphi Magazine has featured two articles on the subject. Each of these references provides a complete description of the development of a custom dataset.

Writing a complete database-oriented custom dataset can be tricky. Writing a simple one to access a directory, however, is something you can easily do once you grasp the basics of dataset programming. Though the component I've written is too detailed to fully describe here, I can give you an overview of the key concepts.

A  custom dataset has to override over 20 virtual abstract methods of the base class, and it generally overrides some additional virtual methods as well. To save some extra coding in the future, I've split my work into two steps. The first is the TListDataSet component, a dataset supporting navigation among the data of a TObjectList. This is one of the new Delphi 5 container classes, which only slightly differs from TList. It uses objects instead of pointers (nothing really different, from a technical point of view) and has the ability to destroy the objects in the list when the list is destroyed or cleared.

Dataset basics

In short, the TListDataSet class contains a list of objects created in the constructor and destroyed by the destructor. The dataset works with record buffers, which store the item numbers in the Index field:

  PRecInfo = ^TRecInfo;
  TRecInfo = record
    Index: Integer;
    Bookmark: Longint;
    BookmarkFlag: TBookmarkFlag;

These records are created in the AllocRecordBuffer method, cleared in InternalInitRecord, and destroyed in the FreeRecordBuffer method. It is a responsibility of the dataset class to determine how many buffers to allocate and call these three methods. The bookmark is basically the same value as the Index, so I could have skipped one of the two. It is handled by the GetBookmarkData and SetBookmarkData methods, while GetBookmarkFlag and SetBookmarkFlag read and write the final field of the TRecInfo structure above. The GetRecNo and SetRecNo methods simply work on the number of the current list item, name FCur, plus one. (The one is added because the record number should start from one while the internal counter is zero-based.)

The only two methods with code are GetRecord and InternalOpen. GetRecord browses the dataset, moving back and forth while simultaneously placing data into the record buffer (in practice, only the list index plus the bookmark information). Here's the complete code for this method:

function TListDataSet.GetRecord
  (Buffer: PChar; GetMode: TGetMode;
  DoCheck: Boolean): TGetResult;
  Result := grOK; // default
  case GetMode of
    gmNext: // move on
      if fCurrent < fList.Count - 1 then
        Inc (fCurrent)
        Result := grEOF; // end of file
    gmPrior: // move back
      if fCurrent &mt; 0 then
        Dec (fCurrent)
        Result := grBOF; // start of file
    gmCurrent: ; // nothing to do

  if Result = grOK then // read the data
    with PRecInfo(Buffer)^ do
      Index := fCurrent;
      BookmarkFlag := bfCurrent;
      Bookmark := Integer (fCurrent);

The InternalOpen method performs a standard sequence of operations: initialize the field definitions, create the field objects, bind them, and initialize the internal structures. It also calls an additional virtual function I've defined, ReadListData, which stores data in the list. This operation will be performed later by the specific subclass.

procedure TListDataSet.InternalOpen;
  // initialize field definitions 
  // and create fields
  if DefaultFields then
  BindFields (True);

  // read directory data

  // initialize
  FRecordSize := sizeof (TRecInfo);
  FCurrent := -1;
  BookmarkSize := sizeOf (Integer);
  FIsTableOpen := True;

Classic derivations

Because I am dealing with a list of objects, the first thing to do in the derived class is to define those objects. In this case, I am working with file data extracted by a TSearchRec buffer by the TFileData class constructor:

TFileData = class
    ShortFileName: string;
    Time: TDateTime;
    Size: Integer;
    Attr: Integer;
    constructor Create(var FileInfo: 

constructor TFileData.Create
  (var FileInfo: TSearchRec);
  ShortFileName := FileInfo.Name;
  Time := FileDateToDateTime 
  Size := FileInfo.Size;
  Attr := FileInfo.Attr;

This constructor is called within the ReadListData method, mentioned earlier, to store data for the files of the current directory:

procedure TDirDataSet.ReadListData;
  Attr: Integer;
  FileInfo: TSearchRec;
  FileData: TFileData;
  // scan all files
  Attr := faAnyFile;
  if SysUtils.FindFirst(fDirectory, 
    Attr, FileInfo) = 0 then
    FileData := TFileData.Create(FileInfo);
  until SysUtils.FindNext(FileInfo) <> 0;

At this point I need to perform two more steps. First, define the fields of the dataset, which in this case are fixed and depend on the available directory data:

procedure TDirDataset.InternalInitFieldDefs;
  // TODO: set proper exception...
  if fDirectory = '' then
    raise Exception.Create('Missing directory');

  // field definitions
  FieldDefs.Add('FileName', ftString, 40, True);
  FieldDefs.Add('TimeStamp', ftDateTime);
  FieldDefs.Add('Size', ftInteger);
  FieldDefs.Add('Attributes', ftString, 3);
  FieldDefs.Add('Folder', ftBoolean);

Second, move the data from the object of the list referenced by the current record buffer (the ActiveBuffer value) to each field of the dataset, as requested by the GetFieldData method:

function TDirDataset.GetFieldData 
  (Field: TField; Buffer: Pointer): 
  FileData: TFileData;
  Bool1: WordBool;
  strAttr: string;
  t: TDateTimeRec;
  FileData := fList 
    as TFileData;
  case Field.Index of
    0: // filename
    1: begin // timestamp
      t := DateTimeToNative(ftdatetime, 
      Move(t, Buffer^, sizeof(TDateTime));
    2:  // size
      Move(FileData.Size, Buffer^, 
    3: begin // attributes
      strAttr := '   ';
      if (FileData.Attr and 
        SysUtils.faReadOnly) > 0 then
        strAttr[1] := 'R';
      if (FileData.Attr and 
        SysUtils.faSysFile) > 0 then
        strAttr[2] := 'S';
      if (FileData.Attr and 
        SysUtils.faHidden) > 0 then
        strAttr[1] := 'H';
      StrCopy(Buffer, pchar(strAttr));
    4: begin // folder
      Bool1 := FileData.Attr and 
        SysUtils.faDirectory > 0;
      Move(Bool1, Buffer^, sizeof(WordBool));
  end; // case
  Result := True;

The tricky part in writing this code was figuring out the internal format of dates stored within date/time fields. This is not the common TDateTime format used by Delphi, but one I can convert to using the DateTimeToNative function. Moving strings and integers is simple. The attributes codes (H for hidden, R for read-only, and S for system) are extracted from the related flags, used also to determine if a file is actually a folder.

Demo delight

With this dataset available, building the demo program was simply a matter of connecting a DBGrid component to it and adding a folder selection (an old-fashioned component still available in Delphi). 

Writing the component wasn't simple, but I doubt I could have written an OLE-DB provider in less time. Writing a custom dataset, although complex, can be managed. I got faster with practice; I built my fourth one quite quickly. To make things easier, use the base class presented in this article as a framework for your own datasets based on lists. The only thing I've skipped is adding support for editing, inserting, and deleting the records, which would require considerable extra effort. I avoided this by defining the dataset as read-only (technically you have to return False from the GetCanModify method).

The complete source code for this custom dataset can be found on my Web site: feel free to use it as a starting point of your own work, and (if you are willing) share the results with me and the community.

Server Response from: ETNASC04