From Part 1:
take an enumerator, mix it with a class helper and pour it over an invokeable custom variant
There it is again. What is he just talking about?
Well, it took me a while to find that thing, as it isn’t mentioned very often. But it turned out to be exactly what I needed. So I dived into the sources and figured out what TInvokeableCustomVariant is all about and how to use it. And even the help was in this case, um, helpful: RAD Studio Help
It turned out that you can use such variants just like classes with properties and methods. Realizing this it sprang directly to my mind: if our enumerator can return such a variant instead of that semi-usefull record index, we can access the record fields like properties:
1 |
data.First_Name |
That was the kind of code I was looking for!
As the class helper seemed to be able to cope with a little bit more, I made it responsible for returning the appropriate variant. The changes were made quickly: the enumerator’s Current property had to be a Variant and the class helper got an additional property CurrentRec also of type Variant, which is used by the enumerator’s GetCurrent method.
You need a couple of things to make your own TInvokeableVariantType descendant work. Obviously we need that descendant:
1 2 3 4 5 6 7 8 |
type TVarDataRecordType = class(TInvokeableVariantType) public procedure Clear(var V: TVarData); override; procedure Copy(var Dest: TVarData; const Source: TVarData; const Indirect: Boolean); override; function GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean; override; function SetProperty(const V: TVarData; const Name: string; const Value: TVarData): Boolean; override; end; |
We will only have one instance of that class, so we need a record type for storing the variant data:
1 2 3 4 5 6 7 |
type TVarDataRecordData = packed record VType: TVarType; Reserved1, Reserved2, Reserved3: Word; DataSet: TDataSet; Reserved4: LongInt; end; |
This is a simplified form of the TVarData record declared in system.pas and as we only have to store a reference to the dataset it can be kept as simple as possible.
At last we need a global variable holding that instance of TVarDataRecordType, a function returning the VarType of that instance and another function creating a Variant of that type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var VarDataRecordType: TVarDataRecordType = nil; function VarDataRecord: TVarType; begin result := VarDataRecordType.VarType; end; function VarDataRecordCreate(ADataSet: TDataSet): Variant; begin VarClear(result); TVarDataRecordData(result).VType := VarDataRecord; TVarDataRecordData(result).DataSet := ADataSet; end; |
The Clear and Copy methods of TVarDataRecordType are pretty simple and just call predefined methods from TCustomVariantType. GetProperty and SetProperty is where the work is done:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function TVarDataRecordType.GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean; var fld: TField; begin { Find a field with the property's name. If there is one, return its current value. } fld := TVarDataRecordData(V).DataSet.FindField(Name); result := (fld <> nil); if result then Variant(dest) := fld.Value; end; function TVarDataRecordType.SetProperty(const V: TVarData; const Name: string; const Value: TVarData): Boolean; var fld: TField; begin { Find a field with the property's name. If there is one, set its value. } fld := TVarDataRecordData(V).DataSet.FindField(Name); result := (fld <> nil); if result then begin { Well, we have to be in Edit mode to do this, don't we? } TVarDataRecordData(V).DataSet.Edit; fld.Value := Variant(Value); end; end; |
The only thing left to do is the implementation of GetCurrentRec from the class helper:
1 2 3 4 |
function TDataSetHelper.GetCurrentRec: Variant; begin Result := VarDataRecordCreate(Self); end; |
That’s it! Now we can write code like:
1 2 3 4 5 6 |
for Employee in QuEmployee do begin S := Trim(Format('%s %s', [Employee.First_Name, Employee.Last_Name])); if Employee.Hire_Date < EncodeDate(1991, 1, 1) then S := '*' + S; MemOutput.Lines.Add(S); end; |
or
1 2 3 4 5 |
for Employee in QuEmployee do begin s := Employee.First_Name; Employee.First_Name := Employee.Last_Name; Employee.Last_Name := s; end; |
Magic, isn’t it?
You can download the complete sources from CodeCentral: 25386
That’s pretty neat.
Would it be possible to have a paramaterized enumerator that uses generics for the dataset, so you could do something like
var
Employee: TEmployee;
begin
for Employee in QuEmployee.Records do
ShowMessage(Employee.Name);
So you would have the benefits of type safety and class completion? The enumerator could use attributes to figure out how to extract the fields into the Temployee class/record.
Interesting idea! Will have a look at it. After all the code is over two years old so it is time for an update.
My comment was supposed be QuEmployee.Records(TEmployee) with greater and less than signs instead of brackets but they got strippd out.
I saw this back when you first showed it on CC and have been using it since. Great work!
Thanks! I felt it was time to go a little more into details.
I investigated adding generic support as I mentioned above, the only problem is that class helpers can’t be parameterized so the enumerator would have to be explicitly created.
Other than that it’s quite simple, just use an attribute to define the database field mapping for the class fields. The enumerator would use RTTI to find this attribute in the class and extract the value from the dataset, and return an instance of the class for each record.