Usage of JvDataSource


How to connect JVCL controls with a DataSet

There are two different kinds of JVCL controls. One that support the TDataSource component and start with the TJvDB prefix. And the other that do not show their data binding in the name. The TJvDB controls can be connected to a DataSet by simply setting their DataSource property to a TDataSource. But the more interesting controls are the so called JvDataSource-aware controls.
These components have a DataConnector property that handles the data binding for the control.

 

 

The DataConnector does not depend on any database code. This allows the component to be data aware when you have a Delphi/BCB/BDS SKU that has the DB.pas unit and to use the component without a DataSet when you have a Personal Edition. This is made possibile by the IJvDataSource interface that is used by the DataConnector to communicate with the TJvDataSource component. The TJvDataSource implements the IJvDataSource interface what TDataSource doesn't do. So it is only possible to connect the DataConnector to a TJvDataSource.

 

Available JvDataSource-aware control

The following components have a DataConnector property:

 

Making an edit control JvDataSource-aware


Decide what DataConnector to use

The following predefined DataConnectors are available:

 

Deriving a specialized DataConnector

After you have choosen a DataConnector base class that fullfill the needs of the control, you must create a decendent of this base class that does the actual data binding for the component. In that decendent you have to implement the RecordChanged and UpdateData virtual methods. The default implementation of ActiveChanged calls RecordChanged. In RecordChanged you have to load the values from the DataSource and write them to the assigned control. The UpdateData method is called whenever the data in the control should be transfered to the DataSource.

As an example the TEdit should be made JvDataSource aware.

type
  TEditEx = class;

  TEditExDataConnector = class(TJvFieldDataConnector)
  private
    FEdit: TEditEx;
  protected
    procedure RecordChanged; override;
    procedure UpdateData; override;
    property Edit: TEditEx read FEdit;
  public
    constructor Create(AEdit: TEditEx);
  end;
The TEditExDataConnector class connects the FieldDataConnector to the TEditEx class.
In the RecordChanged method the value from the data field is copied to the Edit.Text property and the ReadOnly property is set depending on the field's CanModify property. If the field is not valid (Field.IsValue returns False) then the edit is set to readonly mode.
In the UpdateData method the edit's text is written to the field's value property.

constructor TEditExDataConnector.Create(AEdit: TEditEx);
begin
  inherited Create;
  FEdit := AEdit;
end;

procedure TEditExDataConnector.RecordChanged;
begin
  if Field.IsValid then
  begin
    FEdit.ReadOnly := not Field.CanModify;
    FEdit.Text := Field.AsString;
  end
  else
  begin
    FEdit.Text := '';
    FEdit.ReadOnly := True;
  end;
end;

procedure TEditExDataConnector.UpdateData;
begin
  Field.AsString := FEdit.Text;
  FEdit.Text := Field.AsString; // update to stored value
end;

 

Adding the necessary functionality to the edit control

  TEditEx = class(TEdit)
  private
    FDataConnector: TEditExDataConnector;
    procedure SetDataConnector(const Value: TEditExDataConnector);
  protected
    procedure KeyPress(var Key: Char); override;
    procedure DoExit; override;
    procedure Change; override;
    function CreateDataConnector: TEditExDataConnector; virtual;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property DataConnector: TEditExDataConnector read FDataConnector write SetDataConnector;
  end;

The next step is to add the new DataConnector class to the TEdit control. For this we need a DataConnector property that has a setter method. Without the setter Delphi would not store the properties of the DataConnector to the DFM/NFM/XFM. The virtual CreateDataConnector method allow decendants to implement an own DataConnector class.
In the KeyPress method we test if the user has pressed the ESCAPE key and reset the control's data by calling the DataConnector.Reset method.
In the Change method we inform the data source that we want to edit it's data.
The DoExit method transfers the edit's data to the data source by calling DataConnector.UpdateRecord
 
The DataConnector ignores all actions if no JvDataSource is connected to it.

constructor TEditEx.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FDataConnector := CreateDataConnector;
end;

destructor TEditEx.Destroy;
begin
  FDataConnector.Free;
  inherited Destroy;
end;

function TEditEx.CreateDataConnector: TEditExDataConnector;
begin
  Result := TEditExDataConnector.Create(Self);
end;

procedure TEditEx.SetDataConnector(const Value: TEditExDataConnector);
begin
  if Value <> FDataConnector then
    FDataConnector.Assign(Value);
end;

procedure TEditEx.Change;
begin
  DataConnector.Modify;
  inherited Change;
end;

procedure TEditEx.DoExit;
begin
  DataConnector.UpdateRecord;
  inherited DoExit;
end;

procedure TEditEx.KeyPress(var Key: Char);
begin
  inherited KeyPress(Key);
  if (Key = #27) and DataConnector.Active then
  begin
    DataConnector.Reset;
    Key := #0;
  end;
end;

 

Making a list control JvDataSource-aware


Decide what DataConnector to use

In this case we use a TJvFieldDataConnector because the ListBox should display the field values. The ListBox is used as a navigator and as such it does not need to update any data to the data source.

 

Deriving a specialized DataConnector

type
  TJvCustomListBoxDataConnector = class(TJvFieldDataConnector)
  private
    FListBox: TCustomListBox;
    FMap: TList;
    FRecNoMap: TBucketList;
  protected
    procedure Populate; virtual;
    procedure ActiveChanged; override;
    procedure RecordChanged; override;
    property ListBox: TCustomListBox read FListBox;
  public
    constructor Create(AListBox: TCustomListBox);
    destructor Destroy; override;

    procedure GotoCurrent;
  end;

The Populate method iterates the data source's records and fills the Items property of the ListBox.
The GotoCurrent method is called by the CM_CHANGED message handler of the ListBox which is invoked when the ItemIndex has changed.
The two maps are used to find the record and the list item.

constructor TJvCustomListBoxDataConnector.Create(AListBox: TCustomListBox);
begin
  inherited Create;
  FListBox := AListBox;
  FRecNoMap := TBucketList.Create;
  FMap := TList.Create;
end;

destructor TJvCustomListBoxDataConnector.Destroy;
begin
  FMap.Free;
  FRecNoMap.Free;
  inherited Destroy;
end;

procedure TJvCustomListBoxDataConnector.ActiveChanged;
begin
  Populate;
  inherited ActiveChanged;
end;

procedure TJvCustomListBoxDataConnector.GotoCurrent;
begin
  if Field.IsValid and (FListBox.ItemIndex <> -1) then
    DataSource.RecNo := Integer(FMap[FListBox.ItemIndex]);
end;

The ActiveChanged method populates the list when the DataSource' state has changed.
By setting DataSource.RecNo the

procedure TJvCustomListBoxDataConnector.RecordChanged;
var
  Index: Integer;
begin
  if Field.IsValid then
  begin
    if FListBox.Items.Count <> DataSource.RecordCount then
      Populate
    else
      if FRecNoMap.Find(TObject(DataSource.RecNo), Pointer(Index)) then
      begin
        FListBox.Items[Index] := Field.AsString;
        FListBox.ItemIndex := Index;
      end;
  end;
end;

The RecordChanged event either populates the list again if a new record was inserted or a record was deleted, or it updates the list item with a new value and selects the current record.

procedure TJvCustomListBoxDataConnector.Populate;
var
  Index: Integer;
begin
  FMap.Clear;
  FRecNoMap.Clear;
  FListBox.Items.BeginUpdate;
  try
    FListBox.Items.Clear;
    if Field.IsValid then
    begin
      DataSource.BeginUpdate;
      try
        DataSource.First;
        while not DataSource.Eof do
        begin
          Index := FListBox.Items.Add(Field.AsString);
          FMap.Add(TObject(DataSource.RecNo));
          FRecNoMap.Add(TObject(DataSource.RecNo), TObject(Index));
          DataSource.Next;
        end;
      finally
        DataSource.EndUpdate;
      end;
      if FRecNoMap.Find(TObject(DataSource.RecNo), Pointer(Index)) then
        FListBox.ItemIndex := Index;
    end;
  finally
    FListBox.Items.EndUpdate;
  end;
end;

The Populate method fills the list and builds the two maps. Because it wouldn't be a lot of flicker if when the DataDource's records are iterated, the Populate method uses the DataSource.BeginUpdate and DataSOurce.EndUpdate methods. Those save the current record number and disable the controls.

 

Adding the necessary functionality to the list control

type
  TListBoxEx = class(TListBox)
  private
    FDataConnector: TJvCustomListBoxDataConnector;
    procedure SetDataConnector(const Value: TJvCustomListBoxDataConnector);
  protected
    function CreateDataConnector: TJvCustomListBoxDataConnector; virtual;
    procedure CMChanged(var Message: TCMChanged); message CM_CHANGED;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property DataConnector: TJvCustomListBoxDataConnector read FDataConnector write SetDataConnector;
  end;

{ TListBoxEx }

constructor TListBoxEx.Create(AOwner: TComponent);
begin
  inherited Crreate(AOwner);
  FDataConnector := CreateDataConnector;
end;

destructor TListBoxEx.Destroy;
begin
  FDataConnector.Free;
  inherited Destroy;
end;

function TListBoxEx.CreateDataConnector: TJvCustomListBoxDataConnector;
begin
  Result := TJvCustomListBoxDataConnector.Create(Self);
end;

procedure TListBoxEx.CMChanged(var Message: TCMChanged);
begin
  inherited;
  DataConnector.GotoCurrent;
end;

procedure TListBoxEx.SetDataConnector(const Value: TJvCustomListBoxDataConnector);
begin
  if Value <> FDataConnector then
    FDataConnector.Assign(Value);
end;