Sunday, April 8, 2012

How to: on the fly create runtime data aware controls

There are many situations where you have a dataset with unknown fields structure or you just want to popup a small form and present to user some specific fields from the dataset , ie the fields need to be edited after an unsuccessful validation.

So, here is a simple function i have been using for long time ago to create data aware controls on panels, dialogs, scrollboxes:

uses StdCtrls, DB, DbCtrls, TypInfo;

var
  Label_W: integer = 200; {@ Width of the label controls}
  Control_W: integer = 500; {@ Max width of edit controls}
  Ctrl_Ident: integer = 2; {@ Distance between controls (horz & vert)}

function CreateDatasetEditor(
         COwner: TComponent; {@ The owner of control, it will be responsible for destruction}
         WParent: TWinControl; {@ The parent window where controls will live in}
         DSource: TDataSource; {@ TDataSource to be associated with controls}
         const Names, {@ Array of field names to use
               (optional, empty array will use all fields from TDataSource.Dataset}
               Labels: array of string; {@ Array of labels to use
               (optional, empty cells will use field.DisplayLabel }
         X: integer; Y: integer {X,Y coordinates in WParent to start positioning controls}
         ): TRect; {@ Result TRect used to place controls}

var i, j, iHigh: integer;
    c, ic : TControl;
    s: string;
    fld: TField;
    iL,iT: integer;
    Fields: TFields;
    Canvas: TControlCanvas;

 {@ Create a label control}
 procedure CreateDBLabel(ForField: TField; LabelText: string);
 begin
    with TLabel.Create(COwner) do begin
     Parent := WParent;
     AutoSize := False;
     Left := iL + Ctrl_Ident; Inc(iT,Ctrl_Ident); Top:=iT;
     Width := Label_W;
     WordWrap := False;
     if LabelText<>'' then
        Caption := LabelText
     else
        Caption := ForField.DisplayLabel;
     Alignment := taRightJustify;
     AutoSize := True;
     Transparent := True;
     end;
 end;

 {@ Create editing data aware control}
 function CreateEditField(ForFld: TField; sLabel: string): TControl;
 var w, h: integer;
 begin
  {@ Create edit control's associated label}
  CreateDBLabel(ForFld, sLabel);

  {@ Create actual data aware control based on filed info}
  if (ForFld.DataType in [ftBoolean]) then begin
      Result := TDBCheckBox.Create(nil);
      end
  else
  if (ForFld.DataType in [ftMemo, ftFmtMemo]) then begin
      Result := TDBMemo.Create(nil);
      Result.Width := Control_W;
      end
  else
  if (ForFld.FieldKind = fkLookup) then begin
      Result := TDBLookupComboBox.Create(nil);
      end
  else
      begin
      Result := TDBEdit.Create(nil);
      end;

  {@ Insert created control to COwner component hierarchy (for destruction puproses)}
  COwner.InsertComponent(Result);
  {@ Set control parent, width and other properties}
  Canvas.Control := Result;
  Result.Parent := WParent;
  Result.Enabled := not ForFld.ReadOnly;
  case ForFld.DataType of
    ftWord, ftSmallInt, ftInteger, ftAutoInc, ftLargeint: w := Canvas.TextWidth('###,###,###,###,###')+25;
    ftCurrency, ftFloat: w := Canvas.TextWidth('###,###,###,###,##0.00')+25;
    else
    w := ForFld.DisplayWidth * Canvas.TextWidth('W')+50;
    h := Canvas.TextWidth('Wq')+3;
    end;
  if not (ForFld.DataType in [ftMemo, ftFmtMemo]) then
     if w > Control_W then Result.Width := Control_W else Result.Width := w;
  {@ Connect control to DataSource & Field}
  TypInfo.SetOrdProp(Result,'DataSource',LongInt(DSource));
  TypInfo.SetPropValue(Result,'DataField',ForFld.FieldName);
  {@ Final adjustment of control width}
  if Result.Width > Control_W then Result.Width := Control_W;
 end;

 {@ Position a control in sequence}
 procedure PositControl(c: TControl);
 begin
  c.Left := iL + Ctrl_Ident*2 +Label_W; c.Top:=iT; Inc(iT,c.Height);
  Result.Bottom := iT;
  if Result.Right < c.BoundsRect.Right then
    Result.Right := c.BoundsRect.Right;
 end;

begin
 if not Assigned(DSource.DataSet) then Exit;
 Fields := DSource.DataSet.Fields;
 iL:=X;iT:=Y;
 Result.Left := X;
 Result.Top := Y;
 Canvas := TControlCanvas.Create;
 try
 iHigh := High(Labels);
 if Length(Names) > 0 then
    begin // Create controls from Names array
    j:=High(Names);
    for i:=0 to j do begin
      fld := Fields.FindField(Names[i]);
      if Assigned(Fld) then begin
        s:='';
        if (i<=iHigh) then s := Labels[i];
        c := CreateEditField(Fld,s);
        if Assigned(c) then
           PositControl(c);
        end;
      end;
    end
 else
    begin //Create controls from dataset.fields
    j:=Fields.Count-1;
    for i:=0 to j do
      begin
      s:='';
      if (i<=iHigh) then s := Labels[i];
      c := CreateEditField(Fields[i],s);
      if Assigned(c) then
         PositControl(c);
      end;
    end;
 finally Canvas.Free;
 end;
end;


Have fun developing, because development is fun!


Wednesday, April 4, 2012

TParams, a bit deeper with TDatasetRecord

Well, after two very basic and introductory posts about TParams (post 1, post 2), I think it is time to explain in more detail why I am so excited about. It is not about high-end programming, nor about beauty of coding, nor even about state of the art software development. It is just simplicity!

Now that some basic functionality of TParams has been showed off I can go further and deep into using it in data operations.

As I mentioned in other posts and especially in the first, a typical usage of TParams is to hold the values of the fields from a single database record. I also introduced some utility functions that help communication between TParams and the fields of a dataset.

In fact I rarely use such a procedural approach; I have long time ago crucified COBOL ;) .
Delphi is object oriented and as such we have the option to create classes and encapsulate such functionality. Delphi XE2 introduced a nice new feature; class helpers, for extending existing class functionality, but here we do not want to extend the actual TParams class. We will create a new branch with usages beyond what Borland originally designed for this piece of code. Using plain old inheritance we will create a new class, derived from TParams, which will support the two way communication with datasets.

The class should –in general- be able to:
  • Clone and hold data from a given dataset.
  • Process the data without touching the actual dataset ones.
  • Work in an offline scene with data.
  • Upload data to datasets.

And most importantly, these accomplishments should be almost encapsulated in class and sets of classes that can be extended and adapt specific processing needs by using OOP techniques. Such techniques can be to communicate data between classes, modules, programs and network, create hierarchical lists and associations and even relationships.

In order to accomplish these tasks the class should be able to:
  • Store and access data much the same -pretty- way a dataset access its fields.
  • The class should be able to build its fields based on a given TDataset existing fields structure.
  • The class should be able to transfer field values from a given dataset and back to the same or another dataset corresponding fields.

Some extended functionality could be to add a new record or update an existing one to a dataset, or even delete a record.

Well instead of writing a long blog post, I think it is better to deep into my code. So I have created a project at SourceForge.net where you can find the class and a demo project.

Here is the interface part of the class with descriptive remarks for each member:
{@ TDatasetRecord
   Class to represent a dataset record in a TParams collection with each
   TParam serving as a named field/value corresponding to a dataset field}
TDatasetRecord = Class( TParams )
private
  FDataset: TDataset; {@ Dataset associated with the recordclass}
  FUpdateDataset: TDataset; {@ Dataset to update from recordclass data}
  FRecordAvailable, {@ Record class has it's fields (TParams) created and bound them}
  FRecordExists,    {@ Record has beed loaded from a dataset }
  FIncludeAllFields: Boolean; {@ Create all fields from the dataset}
  function GetParam(Index: integer): TParam;
  function GetDataSet: TDataSet;
  procedure SetDataSet(Value: TDataSet);
  function GetUpdateDataSet: TDataSet;
  procedure SetUpdateDataSet(Value: TDataSet);
  function GetFieldValue(const FieldName: string): Variant;
  procedure SetFieldValue(const FieldName: string; const Value: Variant);
protected
  {@ Initializes recordclass and refreshes current recordfield values from Dataset}
  procedure Initialize(RDataset: TDataset; UDataset: TDataset=nil);
  {@ Create recordclass fields }
  procedure CreateFields; virtual;
  {@ Used by derived classes to set a predefined list of allowed
     recordclass fieldnames}
  class function FieldNames(Index: integer): string; virtual;
public
  {@ Construct recordclass connecting to a Dataset, and optional an update dataset}
  constructor Create(RDataset: TDataset; UDataset: TDataset=nil); reintroduce; virtual;
  destructor Destroy; override;
  function AddField(FldType: TFieldType; const FieldName: string): TParam;
  {@ Add recordfields from an object (TDataset, TFIelds, TFieldList, TFieldDefs) }
  function AddFields(Source: TObject): integer;
  {@ Assign, TParams collection override }
  procedure Assign(Source: TPersistent); override;
  {@ Get a new recordclass object cloning self properties, fields & data}
  function CloneRecord: TDatasetRecord;
  {@ Set/Unset recordfields Bound property, Bound=False clears all TParam values }
  procedure BoundRecord(DoBound: Boolean);
  {@ Get a list of recordfields defined in FieldNames string }
  procedure GetFieldList(List: TList; const FieldNames: string);
  {@ Get a FieldNames string stuffed with all recordfield names }
  function GetFieldNames: string;
  {@ Test Target Fields against recordfields for equal values }
  function EqualsTo(Target: TDatasetRecord; UseFields: string = ''): Boolean;
  {@ Set UpdateDataset field values from recordfields }
  function SetDatasetFields: integer; overload;
  {@ Set Target Dataset field values from recordfields }
  function SetDatasetFields(Target: TDataset): integer; overload;
  {@ Set recordfield values from corresponding From Dataset fields }
  procedure SetRecordFields(From: TDataSet; UnBound: Boolean); overload;
  {@ Set recordfield values from corresponding From recordclass fields }
  procedure SetRecordFields(From: TDatasetRecord; UnBound: Boolean); overload;
  {@ Retrieve recordfield values from current dataset record}
  procedure RefreshRecord(DoOpen: Boolean=True);
  {@ recordField access method by name }
  function Field(Value: string): TParam;
  {@ RecordClass has been filled with recordfield values }
  function IsAvailable: Boolean;
  {@ Associated dataset record existed when recordclass filled with values }
  function IsExisting: Boolean;
  property Dataset: TDataset read GetDataset;
  property UpdateDataset: TDataset read GetUpdateDataset write SetUpdateDataset;
  property IncludeAllFields: Boolean read FIncludeAllFields write FIncludeAllFields default True;
  property FieldValues[const FieldName: string]: Variant read GetFieldValue write SetFieldValue; default;
end;
You can download source code files and a simple Datasnap XE2 demo here:  http://users.hol.gr/~georgev/delphi.htm

and at sourceforge project: http://sourceforge.net/projects/datarecords/

Monday, April 2, 2012

How to: Clone TField and TDataset fields structure

This is just a quick tip on how to copy field structure between TDatasets. The interesting part is the "CloneField" function that duplicates the exact class of a TField from one dataset to another.

First the loop that iterates through a source dataset fields collection and clones each item (TField) to  another dataset, the destination. It takes as parameters a source TDataset from where fields structure will be read, a destination TDataset where fields will be created and a boolean that instructs the procedure to add the fields to the existing structure or exactly clone the source structure.

procedure CopyFields(SourceDataset, DestDataset: TDataset; doAdd: Boolean);
var i,p: integer;
    Fld: TField;
    dFld: string;
begin
  if not doAdd then DestDataset.Fields.Clear;
  for i:=0 to SourceDataset.Fields.Count-1 do
    begin
    if Assigned(DestDataset.Fields.FindField(SourceDataset.Fields[i].FieldName)) then
       Continue;
    Fld := CloneField(SourceDataset.Fields[i], DestDataset.Fields.Dataset);
    Fld.DataSet := DestDataset.Fields.Dataset;
    end;
end;

Notice the lines:

Fld := CloneField(SourceDataset.Fields[i], DestDataset.Fields.Dataset);
Fld.DataSet := DestDataset.Fields.Dataset;

The first is the call to "CloneFields" function that creates and returns a new TField object and the second that actually binds the field to the destination dataset. This is required in order to have a functional field in the dataset. Do not rely to the owner of the field that could be any TComponent, ie the form, which is the owner of persistent fields we create with the Delphi form designer.

Now, the function that creates an exact TField descendant class based on another one:

function CloneField(Source: TField; AOwner: TComponent): TField;

  procedure SetProp(Name: string);
  var V: variant;
      PropInfo: PPropInfo;
  begin
   PropInfo := TypInfo.GetPropInfo(Source, Name);
   if PropInfo <> nil then 
     try V := TypInfo.GetPropValue(Source,Name);
      if not VarIsNull(V) then 
         TypInfo.SetPropValue(Result,Name,V); 
     except
      ; //just kill exception
     end;
  end;

begin
  Result := TFieldClass(Source.ClassType).Create(AOwner);

  Result.Alignment              := Source.Alignment;
  Result.AutoGenerateValue      := Source.AutoGenerateValue;
  Result.CustomConstraint       := Source.CustomConstraint;
  Result.ConstraintErrorMessage := Source.ConstraintErrorMessage;
  Result.DefaultExpression      := Source.DefaultExpression;
  Result.DisplayLabel           := Source.DisplayLabel;
  Result.DisplayWidth           := Source.DisplayWidth;
  Result.FieldKind              := Source.FieldKind;
  Result.FieldName              := Source.FieldName;
  Result.ImportedConstraint     := Source.ImportedConstraint;
  Result.LookupDataSet          := Source.LookupDataSet;
  Result.LookupKeyFields        := Source.LookupKeyFields;
  Result.LookupResultField      := Source.LookupResultField;
  Result.KeyFields              := Source.KeyFields;
  Result.LookupCache            := Source.LookupCache;
  Result.ProviderFlags          := Source.ProviderFlags;
  Result.ReadOnly               := Source.ReadOnly;
  Result.Required               := Source.Required;
  Result.Visible                := Source.Visible;

  SetProp('EditMask');
  SetProp('FixedChar');
  SetProp('Size');
  SetProp('Transliterate');
  SetProp('DisplayFormat');
  SetProp('EditFormat');
  SetProp('Currency');
  SetProp('MaxValue');
  SetProp('MinValue');
  SetProp('Precision');
  SetProp('DisplayValues');
  SetProp('BlobType');
  SetProp('ObjectType');
  SetProp('IncludeObjectField');
  SetProp('ReferenceTableName');
  SetProp('Active');
  SetProp('Expression');
  SetProp('GroupingLevel');
  SetProp('IndexName');
end;

The first line of code is the one that creates a new TField descendant from the actual source field class.
Then is the block of base TField common properties assignement, followed by a block of property assignements using runtime  library information (TypInfo) for properties that MAY exist in the actual class. If some of the properties do not exist in the actual class, then they are simply ignored.

Some things to remember:
1.The "doAdd" parameter in "CopyFields" when True results to fields added to the destination fields structure, whilst False forces first to clear the destination fields collection resulting to an exactly same field structure to the destination dataset as the source one.
2.DestDataset has to be inactive in order to call either of the above functions.
3.In "CloneField", if used stand-alone,  "AOwner" represents the TComponent parameter that will be responsible for freeing the field. Usually you will pass the TDataset that the resulting field belongs to, so when the dataset closes it will also be freed.

Have fun developing, cause development is fun!