As you saw in the previous example, a list of objects is conceptually similar to the rows of a table in a dataset. In Delphi, you can build a dataset wrapping a list of objects, as in the case of the TFileData class. It's intriguing to extend this example to build a dataset that supports generic objects, which you can do thanks to the extended RTTI available in Delphi.
This dataset component inherits from TMdListDataSet, as in the previous example. You must provide a single setting: the target class, stored in the ObjClass property (see the complete definition of the TMdObjDataSet class in Listing 17.5).
type TMdObjDataSet = class(TMdListDataSet) private PropList: PPropList; nProps: Integer; FObjClass: TPersistentClass; ObjClone: TPersistent; FChangeToClone: Boolean; procedure SetObjClass (const Value: TPersistentClass); function GetObjects (I: Integer): TPersistent; procedure SetChangeToClone (const Value: Boolean); protected procedure InternalInitFieldDefs; override; procedure InternalClose; override; procedure InternalInsert; override; procedure InternalPost; override; procedure InternalCancel; override; procedure InternalEdit; override; procedure SetFieldData(Field: TField; Buffer: Pointer); override; function GetCanModify: Boolean; override; procedure InternalPreOpen; override; public function GetFieldData(Field: TField; Buffer: Pointer): Boolean; override; property Objects [I: Integer]: TPersistent read GetObjects; function Add: TPersistent; published property ObjClass: TPersistentClass read FObjClass write SetObjClass; property ChangesToClone: Boolean read FChangeToClone write SetChangeToClone default False; end;
The class is used by the InternalInitFieldDefs method to determine the dataset fields based on the published properties of the target class, which are extracted using RTTI:
procedure TMdObjDataSet.InternalInitFieldDefs; var i: Integer; begin if FObjClass = nil then raise Exception.Create ('TMdObjDataSet: Unassigned class'); // field definitions FieldDefs.Clear; nProps := GetTypeData(fObjClass.ClassInfo)^.PropCount; GetMem(PropList, nProps * SizeOf(Pointer)); GetPropInfos (fObjClass.ClassInfo, PropList); for i := 0 to nProps - 1 do case PropList [i].PropType^.Kind of tkInteger, tkEnumeration, tkSet: FieldDefs.Add (PropList [i].Name, ftInteger, 0); tkChar: FieldDefs.Add (PropList [i].Name, ftFixedChar, 0); tkFloat: FieldDefs.Add (PropList [i].Name, ftFloat, 0); tkString, tkLString: FieldDefs.Add (PropList [i].Name, ftString, 50); // TODO: fix size tkWString: FieldDefs.Add (PropList [i].Name, ftWideString, 50); // TODO: fix size end; end;
Similar RTTI-based code is used in the GetFieldData and SetFieldData methods to access the properties of the current object when a dataset field access operation is requested. The huge advantage in using properties to access the dataset data is that read and write operations can be mapped directly to data but also use the corresponding method. This way, you can write the business rules of your application by implementing rules in the read and write methods of the properties—definitely a sounder OOP approach than hooking code to field objects and validating them.
Here is a slightly simplified version of GetFieldData (the other method is symmetric):
function TObjDataSet.GetFieldData ( Field: TField; Buffer: Pointer): Boolean; var Obj: TPersistent; TypeInfo: PTypeInfo; IntValue: Integer; FlValue: Double; begin if FList.Count = 0 then begin Result := False; exit; end; Obj := fList [Integer(ActiveBuffer^)] as TPersistent; TypeInfo := PropList [Field.FieldNo-1]^.PropType^; case TypeInfo.Kind of tkInteger, tkChar, tkWChar, tkClass, tkEnumeration, tkSet: begin IntValue := GetOrdProp(Obj, PropList [Field.FieldNo-1]); Move (IntValue, Buffer^, sizeof (Integer)); end; tkFloat: begin FlValue := GetFloatProp(Obj, PropList [Field.FieldNo-1]); Move (FlValue, Buffer^, sizeof (Double)); end; tkString, tkLString, tkWString: StrCopy (Buffer, pchar(GetStrProp(Obj, PropList [Field.FieldNo-1]))); end; Result := True; end;
This pointer-based code may look terrible, but if you've endured the discussion of the technical details of developing a custom dataset, it won't add much complexity to the picture. It uses some of the data structures defined (and briefly commented) in the TypInfo unit, which should be your reference for any questions about the previous code.
Using this naïve approach of editing the object's data directly, you might wonder what happens if a user cancels the editing operation (something Delphi generally accounts for). My dataset provides two alternative approaches, controlled by the ChangesToClone property and based on the idea of cloning objects by copying their published properties. The core DoClone procedure uses RTTI code similar to what you have already seen to copy all the published data of an object into another object, creating an effective copy (or a clone).
This cloning takes place in both cases. Depending on the value of the ChangesToClone property, either the edit operations are performed on the clone object, which is then copied over the actual object during the Post operation; or the edit operations are performed on the actual object, and the clone is used to get back the original values if editing terminates with a Cancel request. This is the code of the three methods involved:
procedure TObjDataSet.InternalEdit; begin DoClone (fList [FCurrentRecord] as TDbPers, ObjClone); end; procedure TObjDataSet.InternalPost; begin if FChangeToClone and Assigned (ObjClone) then DoClone (ObjClone, TDbPers (fList [fCurrentRecord])); end; procedure TMdObjDataSet.InternalCancel; begin if not FChangeToClone and Assigned (ObjClone) then DoClone (ObjClone, TPersistent(fList [fCurrentRecord])); end;
In the SetFieldData method, you have to modify either the cloned object or the original instance. To make things more complicated, you must also consider this difference in the GetFieldData method: If you are reading fields from the current object, you might have to use its modified clone (otherwise, the user's changes to other fields will disappear).
As you can see in Listing 17.5, the class also has an Objects array that accesses the data in an OOP way and an Add method that's similar to the Add method of a collection. By calling Add, the code creates a new empty object of the target class and adds it to the internal list:
function TMdObjDataSet.Add: TPersistent; begin if not Active then Open; Result := fObjClass.Create; fList.Add (Result); end;
To demonstrate the use of this component, I wrote the ObjDataSetDemo example. It has a demo target class with a few fields and buttons to create objects automatically, as you can see in Figure 17.8. The most interesting feature of the program, however, is something you have to try for yourself. Run the program and look at the DbGrid columns. Then edit the target class, TDemo, adding a new published property to it. Run the program again, and the grid will include a new column for the property.