Delphi Programming Guide
Delphi Programmer 

Menu  Table of contents
Bookmark and Share

Part I - Foundations
  Chapter 1 Delphi 7 and Its IDE
  Chapter 2 The Delphi Programming Language
  Chapter 3 The Run-Time Library
  Chapter 4 Core Library classes
  Chapter 5 Visual Controls
  Chapter 6 Building the User Interface
  Chapter 7 Working with Forms
Part II - Delphi Object-Oriented Architectures
  Chapter 8 The Architecture of Delphi Applications
  Chapter 9 Writing Delphi Components
  Chapter 10 Libraries and Packages
  Chapter 11 Modeling and OOP Programming (with ModelMaker)
  Chapter 12 From COM to COM+
Part III - Delphi Database-Oriented Architectures
  Chapter 13 Delphi's Database Architecture
  Chapter 14 Client/Server with dbExpress
  Chapter 15 Working with ADO
  Chapter 16 Multitier DataSnap Applications
  Chapter 17 Writing Database Components
  Chapter 18 Reporting with Rave
Part IV - Delphi, the Internet, and a .NET Preview
  Chapter 19 Internet Programming: Sockets and Indy
  Chapter 20 Web Programming with WebBroker and WebSnap
  Chapter 21 Web Programming with IntraWeb
  Chapter 22 Using XML Technologies
  Chapter 23 Web Services and SOAP
  Chapter 24 The Microsoft .NET Architecture from the Delphi Perspective
  Chapter 25 Delphi for .NET Preview: The Language and the RTL
  Appendix A Extra Delphi Tools by the Author
  Appendix B Extra Delphi Tools from Other Sources
  Appendix C Free Companion Books on Delphi
  List of Figures    
  List of tables    
  List of Listings    
  List of Sidebars  

Previous Section Next Section

A Dataset of Objects

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).

Listing 17.5: The Complete Definition of the TMdObjDataSet Class
Start example
  TMdObjDataSet = class(TMdListDataSet)
    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);
    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;
    function GetFieldData(Field: TField; Buffer: Pointer): Boolean; override;
    property Objects [I: Integer]: TPersistent read GetObjects;
    function Add: TPersistent;
    property ObjClass: TPersistentClass read FObjClass write SetObjClass;
    property ChangesToClone: Boolean read FChangeToClone
      write SetChangeToClone default False;
End example

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;
  i: Integer;
  if FObjClass = nil then
    raise Exception.Create ('TMdObjDataSet: Unassigned class');
  // field definitions
  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

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;
  Obj: TPersistent;
  TypeInfo: PTypeInfo;
  IntValue: Integer;
  FlValue: Double;
  if FList.Count = 0 then
    Result := False;
  Obj := fList [Integer(ActiveBuffer^)] as TPersistent;
  TypeInfo := PropList [Field.FieldNo-1]^.PropType^;
  case TypeInfo.Kind of
    tkInteger, tkChar, tkWChar, tkClass, tkEnumeration, tkSet:
        IntValue := GetOrdProp(Obj, PropList [Field.FieldNo-1]);
        Move (IntValue, Buffer^, sizeof (Integer));
        FlValue := GetFloatProp(Obj, PropList [Field.FieldNo-1]);
        Move (FlValue, Buffer^, sizeof (Double));
    tkString, tkLString, tkWString:
      StrCopy (Buffer, pchar(GetStrProp(Obj, PropList [Field.FieldNo-1])));
  Result := True;

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;
  DoClone (fList [FCurrentRecord] as TDbPers, ObjClone);
procedure TObjDataSet.InternalPost;
  if FChangeToClone and Assigned (ObjClone) then
    DoClone (ObjClone, TDbPers (fList [fCurrentRecord]));
procedure TMdObjDataSet.InternalCancel;
  if not FChangeToClone and Assigned (ObjClone) then
    DoClone (ObjClone, TPersistent(fList [fCurrentRecord]));

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;
  if not Active then
  Result := fObjClass.Create;
  fList.Add (Result);

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.

Click To expand
Figure 17.8:  The ObjDataSet-Demo example showcases a dataset mapped to objects using RTTI.

Previous Section Next Section



Copyright © 2004-2016 "Delphi Sources". Delphi Programming Guide
     Twitter     Facebook