Getting Data From Anywhere, part 1

Delphi 2006 introduced enumerators – a way to iterate any kind of collection with the for-in loop. And the VCL is chock-full of enumerators: TStrings has one, so do TComponent, TWinControl, TList, TObjectList and the generic TList<T> and TObjectList<T>.

So I recently set about writing a grid control (the main reason for the long delay since my last post). It’s not the first time that I did this and my previous attempt is a nice-looking and quite capable DB-aware grid that replaces TDBGrid.

This time round, I thought it would be cool to take any old data source, not just TDataSource to feed my grid. I wanted to be able to take both TDataSource objects and also any object that will work with the for-in construct.

This has many complications but I thought I’d show three strategies that I came up with to iterate any enumerator and which one I think is better.

If you are not familiar with how enumerators work, here is a quick description from the official documentation:

“To use the forin loop construct on a class or interface, the class or interface must implement a prescribed collection pattern. A type that implements the collection pattern must have the following attributes:

  • The class or interface must contain a public instance method called GetEnumerator(). The GetEnumerator() method must return a class, interface, or record type.
  • The class, interface, or record returned by GetEnumerator() must contain a public instance method called MoveNext(). The MoveNext() method must return a Boolean.
  • The class, interface, or record returned by GetEnumerator() must contain a public instance, read-only property called Current. The type of the Current property must be the type contained in the collection.”

In my simple example, I am going to fill in a TStrings (a TListBox’s Items property to be exact) with text which I get from my enumerator. To simplify matters a little, I’m going to use Delphi’s new ToString property. And I am going to skimp a little on error checking.

The list that I’d like to iterate is this:

type
  TBeatle = class
  private
    FName: string;
  public
    function ToString: string; override;
    constructor Create(const Name: string);
  end;

  TBeatles = class(TObjectList<TBeatle>);

with this trivial implementation:

{ TBeatle }

constructor TBeatle.Create(const Name: string);
begin
  FName := Name;
end;

function TBeatle.ToString: string;
begin
  Result := FName;
end;

and the list is populated with the obvious choices:

Beatles := TBeatles.Create(True);

Beatles.Add(TBeatle.Create('John'));
Beatles.Add(TBeatle.Create('Paul'));
Beatles.Add(TBeatle.Create('George'));
Beatles.Add(TBeatle.Create('Ringo'));

Technique 1: Events

Let us first consider a Delphi 2006-compatible way to step through the list and add its content to a TStrings.

The first thing you’ll need is a pair of event types – one to read the Current property and one to advance the list with MoveNext:

TCallGetCurrentEvent = procedure (Enumerator: TObject; 
  var Value: string) of object;
TCallMoveNextEvent = procedure (Enumerator: TObject; 
  var Result: Boolean) of object;

Actually, both of these should probably be function of object, but I always end up with constructs where the compiler can’t quite determine whether I’m calling the function or taking its address.

Also, here is the declaration of my class method that populates a TStrings with data from an arbitrary enumerator. And yes, I am aware that TStringsFiller is a really, really stupid name.

TStringsFiller = class
public
  class procedure Fill(Strings: TStrings; 
    Enumerator: TObject;
    GetCurrent: TCallGetCurrentEvent;
    MoveNext: TCallMoveNextEvent);
end;

This method is implemented in exactly the way you’d expect:

class procedure TStringsFiller.Fill(Strings: TStrings;
  Enumerator: TObject;
  GetCurrent: TCallGetCurrentEvent;
  MoveNext: TCallMoveNextEvent);
var
  MoveResult: Boolean;
  Current: string;
begin
  MoveNext(Enumerator, MoveResult);
  while MoveResult do
  begin
    GetCurrent(Enumerator, Current);
    Strings.Add(Current);
    MoveNext(Enumerator, MoveResult);
  end;
end;

Usage is pretty simple too:

procedure TForm1.HandleGetCurrent(Enumerator: TObject; 
  var Value: string);
begin
  Value := (Enumerator as TList<TBeatle>.TEnumerator)
    .Current.ToString;
end;

procedure TForm1.HandleMoveNext(Enumerator: TObject; 
  var Result: Boolean);
begin
  Result := (Enumerator as TList<TBeatle>.TEnumerator)
    .MoveNext;
end;

procedure TForm1.OldStyleButtonClick(Sender: TObject);
begin
  TStringsFiller.Fill(ListBox.Items, Beatles.GetEnumerator,
    HandleGetCurrent, HandleMoveNext);
end;

Technique 2: Anonymous Methods

The technique above is quite simple and is very familiar to almost any Delphi programmer. It could be somewhat cumbersome to use though. Needing to add those event handlers and passing them through is the kind of boilerplate code that we all dislike.

So next up is a variation on the above technique, but one that requires Delphi 2009. Instead of the two event types, we have anonymous method types:

TCallGetCurrent = TFunc<TObject, string>;
TCallMoveNext = TFunc<TObject, Boolean>;

For some reason, I don’t get the same compiler confusion when using anonymous functions as I do when using events, so the declarations and the Fill method all become a little simpler:

class procedure TStringsFiller.Fill(Strings: TStrings;
  Enumerator: TObject;
  GetCurrent: TCallGetCurrent; MoveNext: TCallMoveNext);
begin
  while MoveNext(Enumerator) do
    Strings.Add(GetCurrent(Enumerator));
end;

Also, using it is – I think – a bit more intuitive and natural. I guess it is still a little boilerplate, but it just feels a tad less cumbersome to me.

TStringsFiller.Fill(ListBox.Items, Beatles.GetEnumerator,
  function (Enumerator: TObject): string
  begin
    Result := (Enumerator as TList<TBeatle>.TEnumerator)
      .Current.ToString;
  end,
  function (Enumerator: TObject): Boolean
  begin
    Result := (Enumerator as TList<TBeatle>.TEnumerator)
      .MoveNext;
  end);

I could really live with either of these two solutions, but what I wanted was a way to just assign any old list to my Source property and let the property setter code figure it out.

If you’re already guessing what solution I went for, good for you. If not, look out for part 2 in a few days’ time. And here’s a clue:

Perfect timing.

3 Comments »

  1. Mark said

    Hi, FYI the for-in loop doesn’t exist in my Delphi 2006

    • That’s odd? It works for me:-)

      I just double-checked – open classes.pas and search for GetEnumerator. My Delphi 2006 has one for TStrings, one for TList and another for TCollection.

  2. […] 2009 at 16:20 · Filed under Delphi ·Tagged Delphi, enumerators, rtti, the-beatles My last post underwhelmed everyone a little and I understand why. After all, using events or anonymous methods […]

RSS feed for comments on this post · TrackBack URI

Leave a comment