Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works
Anything · Lazarus or Delphi
Random projectRandom project

DataSetEnumerator

TDataSet helper classes to simplify access to TDataSet fields and make TDataSet work with a for-in-loop.

View on GitHub

Member rating

No ratings yet.

Sign in to rate this project.

README

Note:

The only unit is now part or CmonLib: https://github.com/UweRaabe/CmonLibb The latest version can be find over there while this repository will stay as is.

DataSetEnumerator

TDataSet helper classes to simplify access to TDataSet fields and make TDataSet work with a for-in-loop.

The enumerator is implemented using a class helper, which makes it available for all TDataSet descendants. It uses a generic approach allowing type safety as well as code completion.

Let’s have a look at the public section of TDataSetHelper:

delphi
type  
  TDataSetHelper = class helper for TDataSet
  public
    function GetCurrentRec<T: record>: T;
    procedure SetCurrentRec<T: record>(AInstance: T);
    procedure LoadInstanceFromCurrent<T: class>(AInstance: T);
    procedure StoreInstanceToCurrent<T: class>(AInstance: T);
    function Records<T: class>(AInstance: T): IRecords<T>; overload;
    function Records<T: record>: IRecords<T>; overload;
  end;

The first thing to mention is that the enumerator is not exposed by the dataset directly. Instead we have two overloaded functions named Records returning the same generic interface IRecords<T>.

delphi
    IRecords<T> = interface
      function GetEnumerator: TDataSetEnumerator<T>;
    end;

As you see, this interface has no other task than to expose the actual enumerator. The reason is to make use of reference counting to control the lifetime of the inner objects. I also found no other way to get a generic enumerator.

Back to the class helper we notice that one of these functions has a generic constraint record, while the other one has a similar constraint for class, with the addition of taking a parameter with that class type. A similar approach is used for the method pairs GetCurrentRec/SetCurrentRec and LoadInstanceFromCurrent_/StoreInstanceToCurrent_. Besides the enumerator support the class helper obviously allows loading and storing the current dataset record into/from a record or class instance of the given type.

Before diving too much into the implementation details let’s have a look at how this can be used in our code. The example that comes with the sources contains a ClientDataset made up from the employee.xml file which can be found in Delphis samples/data folder.

In the code we declare a record that matches the fields in that dataset.

delphi
type
  TEmployee = record
    EmpNo: Integer;
    LastName: string;
    FirstName: string;
    PhoneExt: string;
    HireDate: TDateTime;
    Salary: Double;
  end;

This is the code using the enumerator to fill a memo with some information about our employees.

delphi
var
  Employee: TEmployee;
  S: string;
begin
  { List all emplyoees with First- and LastName in the Memo. Employees hired
    before 01/01/1991 are marked with an * in front of their names. }
  MemOutput.Lines.BeginUpdate;
  try
    MemOutput.Lines.Clear;
    for Employee in QuEmployee.Records<TEmployee> do begin
      S := Trim(Format('%s %s', [Employee.FirstName, Employee.LastName]));
      if Employee.HireDate < EncodeDate(1991, 1, 1) then
        S := '*' + S;
      MemOutput.Lines.Add(S);
    end;
  finally
    MemOutput.Lines.EndUpdate;
  end;
end;

Now a use case for GetCurrentRec:

delphi
var
  Employee: TEmployee;
begin
  { Show the employee's name and the hire date. }
  Employee := QuEmployee.GetCurrentRec<TEmployee>;
  ShowMessage(Format('%s %s was hired on %s', 
      [Employee.FirstName, Employee.LastName, FormatDateTime('dddddd', Employee.HireDate)]));
end;

The TCustomer class is bound to the customer.xml table and is a bit more sophisticated:

delphi
type
  TCustomer = class
  private
    [DBField('CustNo')]
    FCustNo: Double;
    FCompany: string;
    FAddress1: string;
    FAddress2: string;
    FCity: string;
    FState: string;
    [DBField('Zip')]
    FZipCode: string;
    FCountry: string;
    FPhone: string;
    FFAX: string;
    FTaxRate: Double;
    FContact: string;
    FLastInvoiceDate: TDateTime;
    function GetCustNo: Integer;
    procedure SetCustNo(const Value: Integer);
  public
    [DBField('Addr1')]
    property Address1: string read FAddress1 write FAddress1;
    [DBField('Addr2')]
    property Address2: string read FAddress2 write FAddress2;
    property City: string read FCity write FCity;
    property Company: string read FCompany write FCompany;
    property Contact: string read FContact write FContact;
    property Country: string read FCountry write FCountry;
    [DBField(false)]
    property CustNo: Integer read GetCustNo write SetCustNo;
    property FAX: string read FFAX write FFAX;
    property LastInvoiceDate: TDateTime read FLastInvoiceDate write FLastInvoiceDate;
    property Phone: string read FPhone write FPhone;
    property State: string read FState write FState;
    property TaxRate: Double read FTaxRate write FTaxRate;
    property ZipCode: string read FZipCode write FZipCode;
  end;

There are some attributes controlling the mapping of the properties and fields to the dataset fields. Properties Address1 and Address2 are mapped to the dataset fields Addr1 and Addr2. Property CustNo is not mapped to any field as it is of type integer while the corresponding table field is of type float. Instead we map FCustNo to that dataset field which happens to have the proper type for that. Last FZipCode is mapped to dataset field Zip. The other properties are automatically mapped to their corresponding dataset fields. This is controlled by the mapping mode taken from the global variable DefaultDBFieldsMapping, which is initialized with mapAuto.

delphi
type
  TDBFieldsMapping = (mapAuto, mapManual);

var
  DefaultDBFieldsMapping: TDBFieldsMapping = mapAuto;

The DBFields attribute allows to override this for each class.

delphi
type
  [DBFields(mapManual)]
  TSomeManuallyMappedClass = class
  private
    [DBField('SomeDBFieldName')]
    FSomeField: Integer;
    [DBField('SomeOtherDBFieldName')]
    FSomeOtherField: string;
    FSomeNotMappedField: Double;
  public
    property SomeField: Integer read FSomeField write FSomeField;
    property SomeOtherField: string read FSomeOtherField write FSomeOtherField;
  end;

The enumerator example for TCustomer needs an instance of that class to work with. Due to type inference we can omit the generic type here.

delphi
var
  Customer: TCustomer;
  S: string;
begin
  { List all Customers with their Name and City in the Memo. Customers outside
    the US have their Country enclosed in brackets appended. }
  MemOutput.Lines.BeginUpdate;
  try
    MemOutput.Lines.Clear;
    Customer := TCustomer.Create;
    try
      for Customer in QuCustomer.Records(Customer) do begin
        S := Format('%d: %s - %s %s', [Customer.CustNo, Customer.Company, Customer.Zip, Customer.City]);
        if Customer.Country <> 'US' then
          S := S + ' (' + Customer.Country + ')';
        MemOutput.Lines.Add(S);
      end;
    finally
      Customer.Free;
    end;
  finally
    MemOutput.Lines.EndUpdate;
  end;
end;

Loading and saving data is done in a similar way.

delphi
var
  Customer: TCustomer;
begin
  { Set LastInvoiceDate to current date/time }
  Customer := TCustomer.Create;
  try
    QuCustomer.LoadInstanceFromCurrent(Customer);
    Customer.LastInvoiceDate := Now;
    QuCustomer.Edit;
    QuCustomer.StoreInstanceToCurrent(Customer);
    QuCustomer.Post;
  finally
    Customer.Free;
  end;
end;