Aggregate of multiple UI controls bound to one database column

Viewed 114

I currently have a TDBRadioGroup bound to a CHAR column on the database using the Values property to specify the value stored in the database column for each radio button. Now we have a requirement to add 2 new radio groups along side the existing one that would modify the value. In other words I basically need 3 controls on the form to act as one in a data-aware configuration for the purposes of determining the value stored. A simple example might be if each radio group has a one-character value, and in the database you concatenate them into a 3-character string. Our mapping is more complex than that but that's the general idea. What would be a good way to do this? If someone tells me live bindings would be the way to go, that would be good to know, as it would be help me to convince management to upgrade Delphi. We're stuck on XE.

1 Answers

Interesting q!

I hope I've understood you correctly. Suppose there is a form with 3 radiogroups, one per line, like so

*A B C
D *E F
G H *I

where the asterisks indicates the positions of the selected buttons and suppose that these settings translate into a field value AEI in a row of a dataset. If the G entry of the 3rd RG is clicked, the dataset field value should become GEI. That's what I've assumed you are looking for.

The code below is a "proof of concept" which implements the above functionality.

It does so using 3 standard (non-db-aware) TRadioGroups and an adaptor class TDBRadioGroupAdaptor which makes these operate as db-aware RadioGroups. The TDBRadioGroupAdaptor uses a descendant, TRGFieldDataLink, of the standard TFieldDataLink (see DBCtrls.Pas) to interface with the dataset. I've used a TClientDataSet so the project can be completely self-contained and I've avoided the use of generices as you didn't specify a Delphi version.

Most of the db-aware functionality is contained in the TRGFieldDataLink, which serves to interface the dataset and the RadioGroups and differs from a standard TFieldDataLink in that it treats the field value as composed of N (3 in the code example but it supports an arbitrary number) sub-fields which each have their own RadioGroup. As ever with implementing db-aware functionality, the code for the TDBRadioGroupAdaptor and TRGFieldDataLink is fairly long-winded, as most of it is necessary but not very interesting. About the only ones that are interesting, in that they make the wheels go around, are

function TRGFieldDataLink.GetGroupString : String;

// Returns a string from the ItemIndexes of the RadioGroups

procedure TRGFieldDataLink.GetGroupValues;

//  Sets the DataSet field from the RadioGroup ItemIndexes

which I hope are self-explanatory from the embedded comments.

Because it's only supposed to be a proof-of-concept, the implementation is incomplete, lacking f.i. keyboard support, but that could easily be added by mimicking the source code of the standard TDBRadioGroup.

Obviously, something similar could be used to treat subfields of a dataset field as a group of strings independently editable in a group of TEdits.

Also obviously, it would be possible to turn the TDBRadioGroupAdaptor into a full-blown compound component which includes its own radiogroups, but I've left that as an exercise for the reader.

Code (warning: long!)

type

  TDBRadioGroupAdaptor = class;

  TRGFieldDataLink = class(TFieldDataLink)
  private
    FAdaptor: TDBRadioGroupAdaptor;
    FRecordChanging : Boolean;
    procedure GetGroupValues;
    function GetGroupString: String;
    procedure SetGroupValues(AValue: String);
  public
    constructor Create(AAdaptor : TDBRadioGroupAdaptor);
    destructor Destroy; override;
    procedure RecordChanged(Field : TField); override;
    procedure UpdateData; override;
    property Adaptor : TDBRadioGroupAdaptor read FAdaptor write FAdaptor;
  end;

  TDBRadioGroupAdaptor = class
  private
    FDataLink : TRGFieldDataLink;
    FRadioGroups : TList;
    procedure SetDataSource(Value : TDataSource);
    function GetDataSource : TDataSource;
    function GetRadioGroup(Index: Integer): TRadioGroup;
    procedure ItemClicked(Sender : TObject);
    procedure SetFieldName(const Value: string);
    function GetFieldName : String;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Add(ARadioGroup : TRadioGroup);
    property DataSource : TDataSource read GetDataSource write SetDataSource;
    property FieldName : string read GetFieldName write SetFieldName;
    property RadioGroup[Index : Integer] : TRadioGroup read GetRadioGroup;
  end;

type
  TForm1 = class(TForm)
    DBGrid1: TDBGrid;
    ClientDataSet1: TClientDataSet;
    DataSource1: TDataSource;
    DBNavigator1: TDBNavigator;
    Button1: TButton;
    RadioGroup1: TRadioGroup;
    RadioGroup2: TRadioGroup;
    RadioGroup3: TRadioGroup;
    DBEdit1: TDBEdit;
    DBRadioGroup1: TDBRadioGroup;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
  protected
  public
    Adaptor : TDBRadioGroupAdaptor;
  end;

[...]

{ TRGFieldDataLink }

constructor TRGFieldDataLink.Create(AAdaptor : TDBRadioGroupAdaptor);
begin
  inherited Create;
  Adaptor := AAdaptor;
end;

destructor TRGFieldDataLink.Destroy;
begin
  inherited;
end;

procedure TRGFieldDataLink.SetGroupValues(AValue : String);

//  Sets the ItemIndexes of the RadioGroups by matching each character of AValue
//  to the contents of their Items

var
  i,
  Index : Integer;
  S : String;
begin
  if AValue = '' then Exit; //  To avoid error when CreateDataSet is called
  for i := 0 to Adaptor.FRadioGroups.Count - 1 do begin
    S := AValue[i + 1];
    Index := Adaptor.RadioGroup[i].Items.IndexOf(S);
    Adaptor.RadioGroup[i].ItemIndex := Index;
  end;
end;

procedure TRGFieldDataLink.RecordChanged(Field : TField);

//  called when the dataset goes to a new record, e.g. during scrolling
//  and sets the RadioGroups' ItemIndexes from the dataset data
var
  FieldValue : String;
begin

  Assert(DataSet <> Nil);

  if FRecordChanging then exit; // just in case, avoid re-entrancy

  try
    FRecordChanging := True;
    if Field = Nil then
      Field := DataSet.FieldByName(FieldName);  //  Yukky way of setting Field
    FieldValue :=  Field.AsString;
    SetGroupValues(FieldValue);
  finally
    FRecordChanging := False;
  end;
end;

function TRGFieldDataLink.GetGroupString : String;

// Returns a string from the ItemIndexes of the RadioGroups

var
  i : Integer;
  S : String;
begin
  Result := '';
  for i := 0 to Adaptor.FRadioGroups.Count - 1 do begin
    S := Adaptor.RadioGroup[i].Items[Adaptor.RadioGroup[i].ItemIndex];
    Result := Result +  S [1];
  end;
end;

procedure TRGFieldDataLink.GetGroupValues;

//  Sets the DataSet field from the RadioGroup ItemIndexes

var
  FieldValue,
  S : String;
begin
  Assert(DataSet <> Nil);

  S := Field.AsString;
  FieldValue := GetGroupString;

  Field.AsString := FieldValue;
end;

procedure TRGFieldDataLink.UpdateData;

//  Called by RTL to update the dataset record from the RadioGroups

begin
  GetGroupValues;
end;

{ TDBRadioGroupAdaptor }

procedure TDBRadioGroupAdaptor.Add(ARadioGroup: TRadioGroup);
begin
 FRadioGroups.Add(ARadioGroup);
 ARadioGroup.OnClick := ItemClicked;
end;

constructor TDBRadioGroupAdaptor.Create;
begin
  inherited;
  FRadioGroups := TList.Create;
  FDataLink := TRGFieldDataLink.Create(Self);
end;

destructor TDBRadioGroupAdaptor.Destroy;
begin
  FDataLink.Free;
  FRadioGroups.Free;
  inherited Destroy;
end;

function TDBRadioGroupAdaptor.GetDataSource: TDataSource;
begin
  Result := FDataLink.DataSource;
end;

procedure TDBRadioGroupAdaptor.SetDataSource(Value: TDataSource);
begin
  FDataLink.DataSource := Value;
end;

function TDBRadioGroupAdaptor.GetRadioGroup(Index: Integer): TRadioGroup;
begin
  Result := TRadioGroup(FRadioGroups[Index]);
end;

procedure TDBRadioGroupAdaptor.ItemClicked(Sender: TObject);

//  Responds to one of the RadioGroups being clicked to put the DataSet into Edit state
//  and updates the DataSet field from the ItemIndexes of the RadioGroups

var
  S : String;
begin
  if not FDataLink.FRecordChanging then begin
    S := FDataLink.GetGroupString;
    FDataLink.Edit;
    FDataLink.SetGroupValues(S);
  end;
end;

procedure TDBRadioGroupAdaptor.SetFieldName(const Value: string);
begin
  FDataLink.FieldName := Value;
end;

function TDBRadioGroupAdaptor.GetFieldName: string;
begin
  Result := FDataLink.FieldName;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  Field : TField;
begin

  //  Create 2 fields in the CDS

  Field := TIntegerField.Create(Self);
  Field.FieldName := 'ID';
  Field.FieldKind := fkData;
  Field.DataSet := ClientDataSet1;

  Field := TStringField.Create(Self);
  Field.FieldName := 'Value';
  Field.Size := 40;
  Field.FieldKind := fkData;
  Field.DataSet := ClientDataSet1;


  RadioGroup1.Items.Add('A');
  RadioGroup1.Items.Add('B');
  RadioGroup1.Items.Add('C');

  RadioGroup2.Items.Add('D');
  RadioGroup2.Items.Add('E');
  RadioGroup2.Items.Add('F');

  RadioGroup3.Items.Add('G');
  RadioGroup3.Items.Add('H');
  RadioGroup3.Items.Add('I');

  Adaptor := TDBRadioGroupAdaptor.Create;
  Adaptor.Add(RadioGroup1);
  Adaptor.Add(RadioGroup2);
  Adaptor.Add(RadioGroup3);
  Adaptor.DataSource := DataSource1;
  Adaptor.FieldName := 'Value';

  //  Next, set up the CDS
  ClientDataSet1.CreateDataSet;

  ClientDataSet1.InsertRecord([1, 'AEI']);
  ClientDataSet1.InsertRecord([2, 'BDG']);
  ClientDataSet1.InsertRecord([3, 'ADG']);

end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Adaptor.Free;
end;
Related