DataSetEnumerator
TDataSet helper classes to simplify access to TDataSet fields and make TDataSet work with a for-in-loop.
- Quiet · last change 1-12-2024
- ★ 15 on GitHub
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:
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>.
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.
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.
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:
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:
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.
type
TDBFieldsMapping = (mapAuto, mapManual);
var
DefaultDBFieldsMapping: TDBFieldsMapping = mapAuto;The DBFields attribute allows to override this for each class.
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.
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.
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;
