delphiobject-persistence

How to correctly stream a TCollection property of a subcomponent, e.g. the Columns property of an embedded TDBGrid


I've been trying to boil down to an MCVE some code the author of another q sent me to illustrate a problem with a custom component.

The component is simply a TPanel descendant which includes an embedded TDBGrid. My version of its source, and a test project are below.

The problem is that if the embedded DBGrid has been created with persistent columns, when its test project is re-opened in the IDE, an exception is raised

Error reading TColumn.Grid.Expanded. Property Griddoes not exist.

Executing the Stream method of the test project shows how this problem arises:

For comparison purposes, I also have a normal TDBGrid, DBGrid1, on my form. Whereas the Columns of this DBGrid1 are streamed as

Columns = <
  item
    Expanded = False
    FieldName = 'ID'
    Visible = True
  end
[...]

the embedded grid's columns are streamed like this

Grid.Columns = <
  item
    Grid.Expanded = False
    Grid.FieldName = 'ID'
    Grid.Visible = True
  end
[...]

It's obviously the Grid prefix of Grid.Expanded and the other column properties which is causing the problem.

I imagine that the problem is something to do with the fact that DBGridColumns is a TCollection descendant and that the embedded grid isn't the top-level object in the DFM.

My question is: How should the code of TMyPanel be modified so that the grid's columns get correctly streamed?

Component source:

unit MAGridu;

interface

uses
  Windows, SysUtils, Classes, Controls, ExtCtrls, DBGrids;

type
  TMyPanel = class(TPanel)
  private
    FGrid : TDBGrid;
  public
    constructor Create(AOwner : TComponent); override;
  published
    property Grid : TDBGrid read FGrid;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Standard', [TMyPanel]);
end;

constructor TMyPanel.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FGrid := TDBGrid.Create(Self);
  FGrid.SetSubcomponent(True);
  FGrid.Parent := Self;
end;

end.

Test project source:

type
  TForm1 = class(TForm)
    DBGrid1: TDBGrid;
    CDS1: TClientDataSet;
    DataSource1: TDataSource;
    MyPanel1: TMyPanel;
    Memo1: TMemo;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    procedure Stream;
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  Stream;
end;

procedure TForm1.Stream;
//  This method is included as an easy way of getting at the contents of the project's
//  DFM.  It saves the form to a stream, and loads it into a memo on the form.
var
  SS : TStringStream;
  MS : TMemoryStream;
  Writer : TWriter;
begin
  SS := TStringStream.Create('');
  MS := TMemoryStream.Create;
  Writer := TWriter.Create(MS, 4096);

  try
    Writer.Root := Self;
    Writer.WriteSignature;
    Writer.WriteComponent(Self);
    Writer.FlushBuffer;
    MS.Position := 0;
    ObjectBinaryToText(MS, SS);
    Memo1.Lines.Text := SS.DataString;
  finally
    Writer.Free;
    MS.Free;
    SS.Free;
  end;
end;
end.

procedure TForm1.FormCreate(Sender: TObject);
var
  Field : TField;
begin
  Field := TIntegerField.Create(Self);
  Field.FieldName := 'ID';
  Field.FieldKind := fkData;
  Field.DataSet := CDS1;

  Field := TStringField.Create(Self);
  Field.FieldName := 'Name';
  Field.Size := 20;
  Field.FieldKind := fkData;
  Field.DataSet := CDS1;

  CDS1.CreateDataSet;
  CDS1.InsertRecord([1, 'One']);

end;

end.

Solution

  • Seems there is not much you can do about it. When you look into procedure WriteCollectionProp (local to TWriter.WriteProperties) you see that FPropPath is cleared before the call to WriteCollection.

    The problem with TDBGrid, or better TCustomDBGrid, is that the collection is marked as stored false and the streaming is delegated to DefineProperties, which uses TCustomDBGrid.WriteColumns to do the work.

    Inspecting that method reveals that, although it also calls WriteCollection, the content of FPropPath is not cleared before. This is somewhat expected as FPropPath is a private field.

    The reason why it nonetheless works in the standard use case is that at the moment of writing FPropPath is just empty.

    As even Delphi 10.1 Berlin behaves the same as Delphi 7, I suggest filing a QP report together with just this example.