Skip to main content

Provider Mode: Master-Detail

  • 13 minutes to read

Provider mode in ExpressQuantumGrid allows you to populate a grid control with data from a custom (user-defined) data source. This mode is useful when it is necessary to represent data within a grid control from non data-aware sources (data which cannot be accessed via a TDataSource component).

ExpressQuantumGrid provides unbound mode, which can also be used to represent user-defined data. However, this mode gives you fewer capabilities than provider mode to respond to user editing-aware actions (modifying cell values, inserting and deleting records, etc). In provider mode, it is easier to respond to such actions by implementing appropriate methods of the custom data source. To learn about unbound mode, see the Unbound Mode and Unbound Mode: Master-Detail sections.

To create a custom data source, you need to inherit from the TcxCustomDataSource class and override some of its methods, which are called by a data controller in response to user specific actions.

For instance, you can create a custom data source to represent data from an external file. Each time a user appends a record within a View, the View’s data controller calls the AppendRecord method of your custom data source. You can implement this method in order to add a new entry to the file corresponding to the appended record.

The Provider Mode section shows how to implement provider mode to represent data from a text file. The current document exemplifies the creation of a master-detail relationship in provider mode (both master and detail Views will work in provider mode).

A master-detail relationship associates every single record from a master data source with zero or more records from a detail data source. To set up a master-detail relationship in provider mode, you need to:

  1. Create a hierarchical structure of grid levels and Views similar to the one shown on the following image:

MasterTableView and DetailTableView will represent the master and detail custom data sources respectively.

Refer to the Working with Levels section to learn how to create such a structure at design time and via code. Note that in unbound and provider modes, you should use non data-aware versions of Views (TcxGridTableView, TcxGridBandedTableView and TcxGridCardView).

  1. Create a master custom data source and assign it to the master View’s DataController.CustomDataSource property.

  2. Create a detail custom data source and assign it to the detail View’s DataController.CustomDataSource property.

The last two steps can be performed only via code.

A detail View in ExpressQuantumGrid is represented by its “clones“. A clone is an instance of the detail View displaying records corresponding to a specific master row.

When creating a custom data source, you usually have to override several methods:

The method is responsible for item identification.

Must retrieve the number of records to display within a View. In a detail custom data source, GetRecordCount should return the number of records for a specific detail clone, i.e. according to the currently selected master row.

Returns a value from the custom data source for a specific cell. Before populating a grid View, the data controller calls the GetRecordCount method of the custom data source. Knowing the number of records and items to display, the data controller then calls the GetValue method to populate the View with data. GetValue is called for every cell in the View.

The method is called when the contents of a specific cell is modified. You should implement SetValue to post the modified value to the custom data source.

The method is called when a new record is inserted within a View. You should implement InsertRecord to add a new record to the custom data source.

The method is called when a new record is appended within a View or inserted within a View containing no records. You should implement AppendRecord to add a new record at the end of the custom data source.

The method is called in response to the deletion of a specific record. You should implement DeleteRecord to delete the corresponding record from the custom data source.

Let’s consider a simple example where both master and detail Views work in provider mode. A master View is bound to a custom data source providing data from the TStringList component named MasterStringList. In turn, every entry in this list is bound to a corresponding external (detail) string list via the Objects property. Every string in a detail list is specified in the form: Name=Value. The Name and Value portions are displayed within two columns of a detail View.

The following code shows how to implement the GetRecordCount method of a detail custom data source for this example:

function TUserDetailDataSource.GetRecordCount: Integer;
var
  ADetailStringList: TStringList;
begin
  Result := 0;
  if GetMasterRecordIndex >= 0 then
  begin
    ADetailStringList := 
      TStringList(MasterStringList.Objects[GetMasterRecordIndex]);
    Result := ADetailStringList.Count;
  end;
end;

The function determines the number of records to display according to the record index of the relative master row. GetRecordCount locates the record in MasterStringList corresponding to the master row and obtains a reference to the detail list. The number of records to display is determined by the number of items contained in this list.

The GetMasterRecordIndex function used in this code is defined as follows:

function TUserDetailDataSource.GetMasterRecordIndex: Integer;
begin
  Result := DataController.GetMasterRecordIndex;
end;

This returns the record index of the master row corresponding to the current detail.

The GetValue method of the detail custom data source returns the value of the detail dataset located within the specified record and item. It returns either the Name or the Value portion of the list entry. To get the desired detail list from MasterStringList, the GetMasterRecordIndex function is used.

function TUserDetailDataSource.GetValue(ARecordHandle: TcxDataRecordHandle;
  AItemHandle: TcxDataItemHandle): Variant;
var
  ADetailStringList: TStringList;
  AColumnId: Integer;
  AName: string;
begin
  //Get the column's ID from the visible item index(AItemHandle)
  AColumnID := GetColumnID(Integer(AItemHandle));
  //Get the detail list
  ADetailStringList := TStringList(MasterStringList.Objects[GetMasterRecordIndex]);
  //Return the Name or Value part of the ADetailStringList[ARecordHandle] string
  //based on the column's ID
  AName := ADetailStringList.Names[Integer(ARecordHandle)];
  if AColumnId = 0 then
    Result := AName
  else
    Result := ADetailStringList.Values[AName];
end;

The AItemHandle parameter of the GetValue method determines the visible item’s (column’s) position. This cannot be used as a unique identifier of the column because the column’s position can be changed at runtime. In this example, columns are recognized by their ID‘s. ID is a unique value automatically assigned to a column on creation; the ID of the first column is 0, the ID of the second column is 1, etc. Other methods of item identification are discussed in the Provider Mode document.

The full code of this example is listed below. It shows how to create a grid control, customize it for representing a master-detail relationship, create custom data sources and assign them to appropriate Views. All this is implemented in the following procedures:

  • CreateMasterStringList

Fills MasterStringList and creates detail string lists.

  • CreateGrid

Creates the grid control and the structure of level and View objects.

  • CreateColumns

Creates one column in a master View and two columns in a detail View. Note that in unbound and provider modes the column’s DataBinding.ValueTypeClass property is assigned. This indicates the data type of values to edit within columns.

  • FormCreate (the form’s OnCreate event handler)

Calls the aforementioned procedures, creates master and detail custom data sources and assigns them to relative Views.

The resulting grid control is displayed on the following image:

You can refer to the UnboundExternalDataDemo demonstrating a more elaborate example of implementing a master-detail relationship in provider mode.

The full code of the example.

Note

the implementation of the SetValue, InsertRecord and DeleteRecord methods. They enable the end-user to modify cell values, insert and delete records by pressing the INS and DEL keys respectively.

To run this code:

  • create an empty application

  • replace the code of your Unit1 module with the following code

  • assign the form’s OnCreate and OnDestroy event handlers to the FormCreate and FormDestroy methods respectively

unit Unit1;
interface
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, cxCustomData, cxGrid, cxGridCustomTableView, cxGridTableView, cxDataStorage, cxGridPredefinedStyles;
type
  TUserDataSource = class(TcxCustomDataSource)
  protected
    function AppendRecord: Pointer; override;
    procedure DeleteRecord(ARecordHandle: Pointer); override;
    function GetRecordCount: Integer; override;
    function GetValue(ARecordHandle: TcxDataRecordHandle;
      AItemHandle: TcxDataItemHandle): Variant; override;
    function InsertRecord(ARecordHandle: Pointer): Pointer; override;
    procedure SetValue(ARecordHandle: TcxDataRecordHandle;
      AItemHandle: TcxDataItemHandle; const AValue: Variant); override;
  end;
  TUserDetailDataSource = class(TcxCustomDataSource)
  protected
    function AppendRecord: Pointer; override;
    procedure DeleteRecord(ARecordHandle: Pointer); override;
    function GetColumnID(AItemIndex: Integer): Integer;
    function GetRecordCount: Integer; override;
    function GetValue(ARecordHandle: TcxDataRecordHandle; AItemHandle: TcxDataItemHandle): Variant; override;
    function InsertRecord(ARecordHandle: Pointer): Pointer; override;
    procedure SetValue(ARecordHandle: TcxDataRecordHandle;
      AItemHandle: TcxDataItemHandle; const AValue: Variant); override;
  public
    function GetMasterRecordIndex: Integer;
  end;
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FGrid: TcxGrid;
    FMasterView, FDetailView: TcxGridTableView;
  public
    procedure CreateMasterStringList;
    procedure FreeMasterStringList;
    procedure CreateGrid;
    procedure CreateColumns;
  end;
var
  Form1: TForm1;
implementation
{$R *.dfm}
var
  MasterStringList: TStringList;
{ TUserDataSource }
function TUserDataSource.AppendRecord: Pointer;
begin
  Result := Pointer(MasterStringList.AddObject('', TStringList.Create));
  DataChanged;
end;
procedure TUserDataSource.DeleteRecord(ARecordHandle: Pointer);
begin
  MasterStringList.Objects[Integer(ARecordHandle)].Free;
  MasterStringList.Delete(Integer(ARecordHandle));
  DataChanged;
end;
function TUserDataSource.GetRecordCount: Integer;
begin
  Result := MasterStringList.Count;
end;
function TUserDataSource.GetValue(ARecordHandle: TcxDataRecordHandle;
  AItemHandle: TcxDataItemHandle): Variant;
begin
  Result := MasterStringList[Integer(ARecordHandle)];
end;
function TUserDataSource.InsertRecord(ARecordHandle: Pointer): Pointer;
begin
  MasterStringList.InsertObject(Integer(ARecordHandle), '', TStringList.Create);
  Result := ARecordHandle;
  DataChanged;
end;
procedure TUserDataSource.SetValue(ARecordHandle: TcxDataRecordHandle;
  AItemHandle: TcxDataItemHandle; const AValue: Variant);
begin
  MasterStringList[Integer(ARecordHandle)] := AValue;
end;
{ TUserDetailDataSource }
function TUserDetailDataSource.AppendRecord: Pointer;
var
  ADetailStringList: TStringList;
begin
  ADetailStringList := TStringList(MasterStringList.Objects[GetMasterRecordIndex]);
  Result := Pointer(ADetailStringList.Add(''));
  DataChanged;
end;
procedure TUserDetailDataSource.DeleteRecord(ARecordHandle: Pointer);
var
  ADetailStringList: TStringList;
begin
  ADetailStringList := TStringList(MasterStringList.Objects[GetMasterRecordIndex]);
  ADetailStringList.Delete(Integer(ARecordHandle));
  DataChanged;
end;
function TUserDetailDataSource.GetMasterRecordIndex: Integer;
begin
  Result := DataController.GetMasterRecordIndex;
end;
function TUserDetailDataSource.GetRecordCount: Integer;
var
  ADetailStringList: TStringList;
begin
  Result := 0;
  if GetMasterRecordIndex >= 0 then
  begin
    ADetailStringList := TStringList( 
      MasterStringList.Objects[GetMasterRecordIndex]);
    Result := ADetailStringList.Count;
  end;
end;
function TUserDetailDataSource.GetColumnID(AItemIndex: Integer): Integer;
begin
  Result := TcxCustomGridTableItem(DataController.GetItem(AItemIndex)).ID;
end;
function TUserDetailDataSource.GetValue(ARecordHandle: TcxDataRecordHandle;
  AItemHandle: TcxDataItemHandle): Variant;
var
  ADetailStringList: TStringList;
  AColumnId: Integer;
  AName: string;
begin
  AColumnID := GetColumnID(Integer(AItemHandle));
  ADetailStringList := TStringList(MasterStringList.Objects[GetMasterRecordIndex]);
  AName := ADetailStringList.Names[Integer(ARecordHandle)];
  if AColumnId = 0 then
    Result := AName
  else
    Result := ADetailStringList.Values[AName];
end;
procedure TUserDetailDataSource.SetValue(
  ARecordHandle: TcxDataRecordHandle; AItemHandle: TcxDataItemHandle;
  const AValue: Variant);
var
  ADetailStringList: TStringList;
  AColumnId: Integer;
  S1, S2: string;
begin
  AColumnId := GetColumnID(Integer(AItemHandle));
  ADetailStringList := TStringList(MasterStringList.Objects[GetMasterRecordIndex]);
  if AColumnId = 0 then
  begin
    if VarIsNull(AValue) then
      S1 := ''
    else
      S1 := AValue;
      S2 := ADetailStringList.Values[ 
        ADetailStringList.Names[Integer(ARecordHandle)]]
    end
  else
  begin
    S1 := ADetailStringList.Names[Integer(ARecordHandle)];
    if VarIsNull(AValue) then
      S2 := ''
    else
      S2 := AValue;
  end;
  ADetailStringList[Integer(ARecordHandle)]:= Format('%S=%S', [S1,S2]);
end;
function TUserDetailDataSource.InsertRecord(ARecordHandle: Pointer): Pointer;
var
  ADetailStringList: TStringList;
begin
  ADetailStringList := TStringList(MasterStringList.Objects[GetMasterRecordIndex]);
  ADetailStringList.Insert(Integer(ARecordHandle), '');
  Result := ARecordHandle;
  DataChanged;
end;
{ TForm1 }
procedure TForm1.CreateGrid;
begin
  FGrid := TcxGrid.Create(Self);
  with FGrid do
  begin
    FMasterView := CreateView(TcxGridTableView) as TcxGridTableView;
    Levels.Add.GridView := FMasterView;
    FDetailView := CreateView(TcxGridTableView) as TcxGridTableView;
    Levels[0].Add.GridView := FDetailView;
    Align := alClient;
    Parent := Self;
  end;
end;
procedure TForm1.CreateColumns;
begin
  with FMasterView.CreateColumn do
  begin
    DataBinding.ValueTypeClass := TcxStringValueType;
    Caption := 'Planet';
    Name := 'mvPlanet';
    Width := 300;
  end;
  with FDetailview.CreateColumn do
  begin
    DataBinding.ValueTypeClass := TcxStringValueType;
    Caption := 'Satellite';
    Name := 'mvSatellite';
    Width := 150;
  end;
  with FDetailview.CreateColumn do
  begin
    DataBinding.ValueTypeClass := TcxStringValueType;
    Caption := 'Distance';
    Name := 'mvDistance';
    Width := 150;
  end;
end;
procedure TForm1.CreateMasterStringList;
var
  ADetail1, ADetail2: TStringList;
begin
  ADetail1 := TStringList.Create;
  with ADetail1 do
  begin
    Add('Despina=53');
    Add('Galatea=62');
    Add('Proteus=118');
  end;
  ADetail2 :=TStringList.Create;
  with ADetail2 do
  begin
    Add('Cordelia=50');
    Add('Ophelia=54');
    Add('Bianca=59');
    Add('Cressida=62');
    Add('Desdemona=63');
  end;
  MasterStringList := TStringList.Create();
  with MasterStringList do
  begin
    AddObject('Neptune', ADetail1);
    AddObject('Uranus', ADetail2);
  end;
end;
procedure TForm1.FreeMasterStringList;
var
  I: Integer;
begin
  for I := 0 to MasterStringList.Count - 1 do
    MasterStringList.Objects[I].Free;
  MasterStringList.Free;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
  CreateMasterStringList;
  CreateGrid;
  CreateColumns;
  FMasterView.DataController.CustomDataSource := TUserDataSource.Create;
  FDetailView.DataController.CustomDataSource := TUserDetailDataSource.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeMasterStringList;
end;
end.