A couple of years ago I wrote a two-part article about a dataset enumerator: A Magical Gathering – Part 1 and Part 2. Well, things evolved a bit since then and I wondered how one would implement something similar given the current features of Delphi. Actually I am following here a suggestion from commenter Alan Clark.
The enumerator is implemented quite similar to the original version using a class helper, which makes it available for all TDataSet descendants. The variant part is completely replaced with a generic approach allowing type safety as well as code completion.
Let’s have a look at the public section of TDataSetHelper:
1 2 3 4 5 6 7 8 9 |
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 no longer exposed by the dataset directly. Instead we now have two overloaded functions named Records returning the same generic interface IRecords<T>.
1 2 3 |
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.
1 2 3 4 5 6 7 8 9 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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:
1 2 3 4 5 6 7 8 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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.
The enumerator example for TCustomer needs an instance of that class to work with.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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<TCustomer>(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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var Customer: TCustomer; begin { Set LastInvoiceDate to current date/time } Customer := TCustomer.Create; try QuCustomer.LoadInstanceFromCurrent<TCustomer>(Customer); Customer.LastInvoiceDate := Now; QuCustomer.Edit; QuCustomer.StoreInstanceToCurrent<TCustomer>(Customer); QuCustomer.Post; finally Customer.Free; end; end; |
The source code of TDatasetHelper with a small example can be downloaded here: Dataset Enumerator .
Thank you Uwe Raabe for the beauty library.
It does not compile under Delphi XE7.
Could you check the issue?
That is a limitation of XE7 probably related to RSP-10068 which was fixed in XE8.
You may need to flatten the nested types to make it compile with XE7 and below.
Thank you for the explanation
Thank you very much. We had similar functionality but not implemented in such an elegant way. This should be built into TDataset.
Very nice, clean and useful.