4

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.
MartynA
  • 30,454
  • 4
  • 32
  • 73
  • 1
    Just wondering: why do you set the parent of the embedded grid to the parent of the underlying MyPanel? During TMyPanel.Create the grid is parented to the MyPanel. Shouldn't it stay that way? – Uwe Raabe Jul 10 '16 at 20:09
  • @UweRaabe: Thanks for the interest. "During TMyPanel.Create the grid is parented to the MyPanel." Not sure I follow, on exit from TMyPanel.Create the parent of FGrid is Nil (in D7 at any rate) and so without the assignment in TMyPanel.SetParent, the Grid is invisible. – MartynA Jul 10 '16 at 20:21
  • @MartynA: Is your intention to have the `TDBGrid` always be a child of your Panel? If so, then the Panel's constructor should have a `FGrid.Parent := Self;` statement and the `SetParent()` override should be removed completely. – Remy Lebeau Jul 10 '16 at 20:39
  • Thanks @RemyLebeau. I've updated the TMyPanel constructor as you suggested, in my code and in my q. Unfortunately, the problem with streaming persisted TColumns remains. – MartynA Jul 10 '16 at 20:50
  • I am truly curious if you have failed to correctly stream the collection property of the subcomponent using the code in my answer. Can you comment? – Sertac Akyuz Jul 13 '16 at 15:41
  • @SertacAkyuz: Sorry for the delay. I got part way through trying out your answer and then got distracted by a car crash. I tried the first part of your answer, up to TestPanel2, in D7 (haven't tried Seattle yet). I can set up persistent fields in the IDE, but nothing about the grid is saved in the DFM. Consequently, the grid stays empty at r/time. Of course, if I assign the grid's DataSource to the one on my form at r/time, it creates dynamic columns. I'll try out your TestPanel2 sometime tomorrow. – MartynA Jul 13 '16 at 15:57
  • @Martyn - Sorry for the accident. Take your time. I'm good as long as I know you'll test it. – Sertac Akyuz Jul 13 '16 at 15:58
  • @SertacAkyuz: I just tested your TMyPanel2 in D& and Seattle and it works fine with persisted TColumns, both at r/time and when re-opening the project. So +1. Interestingly the dfm contains both a reference to the grid as a property of the panel and also the grid as a sub-object object MyPanel21: TMyPanel2[...] Grid = MyPanel21.InternalDBGrid object InternalDBGrid: TDBGrid [...] Columns = < item Expanded = False FieldName = 'ID' Visible = True end – MartynA Jul 14 '16 at 18:22
  • @Martyn > *" Interestingly ..."* > Yeah, that's how I could make it work. The 'Grid' reference of 'TMyPanel' can be eliminated with a `stored False` identifier, but then grid's properties are not expanded in the OI. Thanks for the vote. As for being the correct answer of your question, IMO, the currently accepted answer is evidently *not correct*. And my solution probably needs more thorough testing, maybe asker of the original question does it. I'd leave the question unanswered if I were you, but of course that's up to you. – Sertac Akyuz Jul 14 '16 at 18:45

2 Answers2

2

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.

Uwe Raabe
  • 45,288
  • 3
  • 82
  • 130
  • Many thanks. I've been using D7 simply because I wasn't sure what the OP of the original q was using. I'll check it behaves similarly in Seattle (Berlin not installed yet) then submit a QP report as you suggest. – MartynA Jul 10 '16 at 21:14
1

The solution would involve the embedded grid not having the form that owns the panel as the streaming root, but the panel itself. This will prevent the grid's properties being qualified by 'Grid', which, in effect, will eliminate column properties being wrongly qualified by the same. That is to say, the below is a workaround for faulty behavior.

To achieve the above, remove the SetSubComponent call,

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

The csSubComponent style being removed, now the grid is not streamed at all.

Then override GetChildren for the panel to stream the grid through the panel. GetChildren, as documented, is used to determine which child controls are saved (streamed) of a control. Since we have only one control (the grid) we don't need to make a distinction and instead can call the inherited handler modifying the root.

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

...

procedure TMyPanel.GetChildren(Proc: TGetChildProc; Root: TComponent);
begin
  inherited GetChildren(Proc, Self);
end;


Then remains resolving subcomponent complications. Complication here was a second grid being created sitting in front of the panel which assumes streamed properties. Very much like in this unanswered question. Note that this problem is not related to the solution provided above. The original code displays the same problem.

Having read the question mentioned above, and this one, and this one, and this one, and still not being able to resolve with the help of the code, clues, advices in them, I traced the streaming system and came up with my solution as below.

I'm not claiming it is how it is supposed to be. It is just how I could make this to work. Main modifications are, the sub-grid is now writable (which would require a setter in production code), the conditional creation of the grid, and the overriden GetChildOwner of the panel. Below is the entire unit having TMyPanel2 (TMyPanel couldn't make it... ).

unit TestPanel2;

interface

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

type
  TMyPanel2 = class(TPanel)
  private
    FGrid : TDBGrid;
  protected
    function GetChildOwner: TComponent; override;
  public
    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;
    procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
  published
    property Grid : TDBGrid read FGrid write FGrid;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Test', [TMyPanel2]);
end;

constructor TMyPanel2.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  if not (csReading in AOwner.ComponentState) then begin
    FGrid := TDBGrid.Create(Self);
    FGrid.Name := 'InternalDBGrid';
    FGrid.Parent := Self;
  end else
    RegisterClass(TDBGrid);
end;

destructor TMyPanel2.Destroy;
begin
  FGrid.Free;
  inherited;
end;

function TMyPanel2.GetChildOwner: TComponent;
begin
  Result := Self;
end;

procedure TMyPanel2.GetChildren(Proc: TGetChildProc; Root: TComponent);
begin
  Proc(Grid);
end;

end.
Community
  • 1
  • 1
Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169