Collection Properties

Collection Properties

At times you need a property holding a list of values, not a single value. Sometimes you can use a TStringList-based property, but it accounts only for textual data (even though an object can be attached to each string). When you need a property hosting an array of objects, the most VCL-sound solution is to use a collection. The role of collections, by design, is to build properties that contain a list of values. Examples of Delphi collection properties include the DBGrid component's Columns property, and the TStatusBar component's Panels property.

A collection is basically a container of objects of a given type. For this reason, to define a collection, you need to inherit a new class from the TCollection class and also inherit a new class from the TCollectionItem class. This second class defines the objects held by the collection; the collection is created by passing to it the class of the objects it will hold.

Not only does the collection class manipulate the items of the collection, but it is also responsible for creating new objects when its Add method is called. You cannot create an object and then add it to an existing collection. Listing 9.3 shows two classes for the items and a collection, with their most relevant code.

Listing 9.3: The Classes for a Collection and Its Items
Start example
type
  TMdMyItem = class (TCollectionItem)
  private
    FCode: Integer;
    FText: string;
    procedure SetCode(const Value: Integer);
    procedure SetText(const Value: string);
  published
    property Text: string read FText write SetText;
    property Code: Integer read FCode write SetCode;
  end;
   
  TMdMyCollection = class (TCollection)
  private
    FComp: TComponent;
    FCollString: string;
  public
    constructor Create (CollOwner: TComponent);
    function GetOwner: TPersistent; override;
    procedure Update(Item: TCollectionItem); override;
  end;
   
{ TMdMyCollection }
   
constructor TMdMyCollection.Create (CollOwner: TComponent);
begin
  inherited Create (TMdMyItem);
  FComp := CollOwner;
end;
   
function TMdMyCollection.GetOwner: TPersistent;
begin
  Result := FComp;
end;
   
procedure TMyCollection.Update(Item: TCollectionItem);
var
  str: string;
  i: Integer;
begin
  inherited;
  // update everything in any case...
  str := '';
  for i := 0 to Count - 1 do
  begin
    str := str + (Items [i] as TMyItem).Text;
    if i < Count - 1 then
      str := str + '-';
  end;
  FCollString := str;
end;
End example

The collection must define the GetOwner method to be displayed properly in the collection property editor provided by the Delphi IDE. For this reason, it needs a link to the component hosting it, the collection owner (stored in the FComp field in the code). You can see this sample component's collection in Figure 9.11.

Click To expand Figure 9.11:  The collection editor, with the Object TreeView and the Object Inspector for the collection item

Every time data changes in a collection item, its code calls the Changed method (passing True or False to indicate whether the change is local to the item or refers to the entire set of items in the collection). As a result of this call, the TCollection class calls the virtual method Update, which receives as a parameter the single item requesting the update, or nil if all items changed (and when the Changed method is called with True as a parameter). You can override this method to update the values of other elements of the collection, of the collection itself, or of the target component.

In this example you update a string with a summary of the collection data that you've added to the collection and that the host component will surface as a property. Using the collection within a component is simple. You declare a collection, create it in the constructor and free it at the end, and expose it through a property:

type
  TCanTest = class(TComponent)
  private
    FColl: TMyCollection;
    function GetCollString: string;
  public
    constructor Create (aOwner: TComponent); override;
    destructor Destroy; override;
  published
    property MoreData: TMyCollection read FColl write SetMoreData;
    property CollString: string read GetCollString;
  end;
   
constructor TCanTest.Create(aOwner: TComponent);
begin
  inherited;
  FColl := TMyCollection.Create (Self);
end;
   
destructor TCanTest.Destroy;
begin
  FColl.Free;
  inherited;
end;
   
procedure TCanTest.SetMoreData(const Value: TMyCollection);
begin
  FColl.Assign (Value);
end;
   
function TCanTest.GetCollString: string;
begin
  Result := FColl.FCollString;
end;

Notice that the collection items are streamed in DFM files along with the component hosting them, using the special item markers and angle brackets, as in the following example:

object MdCollection1: TMdCollection
  MoreData = <
    item
      Text = 'one'
      Code = 1
    end
    item
      Text = 'two'
      Code = 2
    end
    item
      Text = 'three'
      Code = 3
    end>
end


Part I: Foundations