1

I'm having problems adding my own TCollectionItem classes (inherited from TCollectionItem) within the same TOwnedCollection.

I referred to Indy's IdMessageParts.pas for TIdMessagePart as recommended. So I must be missing something as I get a "Invalid class type" error when adding TMyItem2.

I would like MyItems: TOwnedCollection to be able to store TMyItem, TMyItem2, TMyItem3. But I get an "Invalid typecast" error when adding TMyItem2 and TMyItem3 (only TMyItem can be accepted).

Did I miss something?

TMyItem = class(TCollectionItem)
private
  FLabelName: string;
public
  constructor Create(Collection: TCollection); override;
  destructor Destroy; override;
published
  property LabelName: string read FLabelName write FLabelName;
end;

TMyItem2 = class(TMyItem)
private
  FCaption: string;
published
  property Caption: string read FCaption write FCaption;
end;

TMyItem3 = class(TMyItem)
private
  FCaption3: string;
published
  property Caption3: string read FCaption3 write FCaption3;
end;

TMyItems = class(TOwnedCollection)
private
  function GetMyItem(aIndex: Integer): TMyItem;
protected
  constructor Create(aOwner: TAsapListview);
  function GetOwner: TPersistent; override;
public
  function Add: TMyItem;
  function IndexOf(aFieldName: string): Integer;
  function MyItemByFieldName(aFieldName: string): TMyItem;
  property Items[aIndex: Integer]: TMyItem read GetMyItem; default;
end;

// NOTE: in idMessageParts.pas the add is defined this way
// which I don't quite understand
{
function TIdMessageParts.Add: TIdMessagePart;
begin
  // This helps prevent TIdMessagePart from being added
  Result := nil;
end;
}

// this is the calling code
with MyItems.Add do
begin
  LabelName := 'test';  // works
end;
// Error of invalid typecast occurred here
with MyItems.Add as TMyItem2 do  // typecast error here
begin
  LabelName := 'label item2';
  Caption := 'caption item2';
end;
with MyItems.Add as TMyItem3 do  // typecast error here
begin
  LabelName := 'label item2';
  Caption := 'caption item2';
  Caption3 := 'caption3 item2';
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Peter Jones
  • 451
  • 2
  • 12

1 Answers1

2

The inherited TCollection.Add() method creates an instance of the class type that is specified in the TCollection constructor. So Add() is useless when dealing with multiple TCollectionItem-derived classes, since you can't add multiple classes that way.

This code:

with MyItems.Add do
begin
 ...
end;

Works because you told TMyItems that its item class type is TMyItem, so that is what Add() creates.

This code:

with MyItems.Add as TMyItem2 do
...
with MyItems.Add as TMyItem3 do

Fails for the same reason. You told TMyItems to create TMyItem objects, so you can't cast them to your other class types.

Indy's TIdMessageParts collection overrides Add() to dissuade users from using Add() in this manner.

To accomplish what you want (and what Indy does), you need to call the TCollectionItem constructor instead of using Add(), eg:

with TMyItem.Create(MyItems) do
begin
  LabelName := 'test';  // works
end;

with TMyItem2.Create(MyItems) do
begin
  LabelName := 'label item2';
  Caption := 'caption item2';
end;

with TMyItem3.Create(MyItems) do
begin
  LabelName := 'label item2';
  Caption := 'caption item2';
  Caption3 := 'caption3 item2';
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • I saw the light. I didn't know TCollectionItem can be created this way. Much appreciated for the sample code and the precise clarification! – Peter Jones Jun 02 '22 at 05:25
  • However, if I save the component to a stream and restore it, it gives a Caption / Caption3 error : Error reading TMyComponent.MyItems: Error reading TMyItem.Caption3: Property does not exist. – Peter Jones Jun 02 '22 at 06:20
  • @PeterJones correct, because the native DFM streaming system only knows how to stream a `TCollection` using its constructor-specified item class, not multiple derived classes. When a `TCollection` is written to a DFM stream, all of the item properties are written as expected, since they are known to RTTI. But when reading a `TCollection` from a DFM stream, `TCollection.Add()` is called for each item, and the stored properties are read into each item. That is why you are getting an error about a derived class property not existing in a base class. – Remy Lebeau Jun 02 '22 at 17:05
  • @PeterJones If you want to store multiple derived class items in a stream, you will have to manually write/read the `TCollection` to/from the stream, not using the native DFM streaming. That way, you can write each item's `ClassName` to the stream so you can read it back out and know which derived class to create for each item. Indy's `TIdMessageParts` collection doesn't go that far, because its items are never created at design-time, only at runtime, and thus are never streamed. – Remy Lebeau Jun 02 '22 at 17:14
  • thanks for the pointer. Looks like a very involved process. Just surprised no Delphi component made use of this feature. I wish Delphi streaming will take care of it since it allows multiple derived class items. – Peter Jones Jun 02 '22 at 17:50
  • @PeterJones using multiple derived classes in a `TCollection` is actually quite rare, which is why the native DFM system was never setup to handle it. It would take only a few lines of code in the RTL for Embarcadero to implement this natively, but they haven't done that yet. – Remy Lebeau Jun 02 '22 at 17:51
  • Is it safe to say that Collections and CollectionItems are not meant to be used with multiple derived classes? I moved to TPersistent, and using an ObjectList - because im not going to create these items at designtime (for now). – eXXecutor Jun 12 '22 at 14:49
  • 1
    @eXXecutor **Using** multiple derived item classes is fine. **Streaming** them is not (easily). But, if you are not using these classes at design-time, or in streams at runtime, then this issue is moot. – Remy Lebeau Jun 12 '22 at 22:11