# 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:

- TJvEdit
- TJvValidateEdit
  Supports NullValue when the field is NULL
- TJvComboEdit
- TJvDateEdit
  Supports DefaultDate and DefaultDateIsNow when the field is NULL
- TJvCheckBox
- TJvCheckListBox
  Selects a row and the checked state can be written to another JvDataSource
- TJvHTListBox
  Selects a row

# Making an edit control JvDataSource-aware

###

## Decide what DataConnector to use

There are the following predefined DataConnectors available:

- TJvDataConnector: Connects the whole DataSet to the control
- TJvFieldDataConnector: Connects a single field to the control (e.g. TJvEdit)
- TJvLookupDataConnector: Connects a lookup list to the control (e.g. TJvCheckListBox)

## 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 <ii>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;