Archive for September, 2009

Getting Data From Anywhere, part 2

My last post underwhelmed everyone a little and I understand why. After all, using events or anonymous methods for this kind of thing is standard fare and it doesn’t even do the job that I set out to do in the first place.

It works, in that I am able to take data from any enumerator, but it is unintuitive to use and requires the use of boilerplate code.

Well, you’d be happy to know that I was also unhappy with these solutions (hence the previous post was called “Part 1”) and I desperately wanted to get it to work with RTTI instead. Because if I could just grab pointers to the enumerator’s MoveNext method and Current property you could completely obviate the need for code being passed from the caller.

Unfortunately, Delphi’s RTTI had a deal-breaking limitation, namely RTTI was only generated for published members. None of the GetEnumerator methods I could find was published and none of those enumerators had published MoveNext methods and Current properties.

Just as I was ready to resign myself to using the techniques described in part 1, Delphi 2010 became available. Hurray! I immediately set to work to use the new RTTI system to provide the alternative that I wanted and found it dead-easy.

So, without further delay, here is the new and improved way of getting data from anywhere.

Technique 3: Delphi 2010 RTTI

We needed to do two things, remember? First, find the public MoveNext method which takes no parameters and returns a Boolean. Second, find the public Current property which returns a type we can figure out how to use.

I’ve decided not to give an exhaustive description of how the new RTTI system works, because Robert Love has done such a fabulous job demystifying it in the last week or so. Go check out some of his posts to delve deeper into the inner workings.

Here is my complete method:

   1  class procedure TStringsFiller.Fill(Strings: TStrings;
   2    Enumerator: TObject);
   3  var
   4    Context: TRttiContext;
   5    EnumType: TRttiType;
   6    Current: TRttiProperty;
   7    MoveNext: TRttiMethod;
   8    Value: TValue;
   9  begin
  10    Context := TRttiContext.Create;
  11    try
  12      EnumType := Context.GetType(Enumerator.ClassType);
  13  
  14      // Find the Current property
  15      Current := EnumType.GetProperty('Current');
  16      if (Current = nil) or
  17        not (Current.PropertyType.TypeKind in
  18          [tkString, tkUString, tkClass]) then
  19        raise Exception.Create('Invalid Current property');
  20  
  21      // Find the MoveNext property
  22      MoveNext := EnumType.GetMethod('MoveNext');
  23      if (MoveNext = nil) or (Length(MoveNext.GetParameters) > 0) or
  24        (MoveNext.MethodKind <> mkFunction) or
  25        (MoveNext.ReturnType.Handle <> TypeInfo(Boolean)) then
  26        raise Exception.Create('Invalid MoveNext method');
  27  
  28      // while MoveNext do
  29      while MoveNext.Invoke(Enumerator, []).AsBoolean do
  30      begin
  31        // Value := Current
  32        Value := Current.GetValue(Enumerator);
  33        case Value.Kind of
  34          tkClass: Strings.Add(Value.AsObject.ToString);
  35          tkUString, tkString: Strings.Add(Value.AsString);
  36          tkClassRef: Strings.Add(Value.AsClass.ClassName);
  37          // Any other types you want to support go here
  38        end;
  39      end;
  40    finally
  41      Context.Free;
  42    end;
  43  end;

We have a fair bit more code than previously, so let’s do a quick walkthrough. Lines 10 and 12 are the standard statements you need to get your hands on the TRttiType object that contains all the good stuff.

Lines 15 through 19 gets the Current property and checks that it is of one of the types we’re going to use in this example. If not, we raise an exception. Here we could also add code to check that the property is not write-only and that it is not indexed.

Lines 22 through 26 gets the MoveNext method. It has to be a zero-parameter function that returns Boolean or we’re simply not interested.

There is an interesting scenario that I have not catered for here – what if the object has multiple overloaded MoveNext methods? My tests show that this code will then only pick up the one that was declared first.

To correctly handle that scenario, we really should call EnumType.GetMethods and iterate the returned array to find the method with the matching signature.

Finally, lines 29 through 39 use the returned MoveNext and Current to construct the loop we need to fill the list. Notice that we’re getting a TValue back from Current.GetValue(Enumerator) and that we need to check individually for all the types we know how to use – strings, objects and class references in this case.

Having much more code in the Fill method does have its payoff though: Calling the function is now far nicer and requires absolutely no boilerplate:

TStringsFiller.Fill(ListBox.Items, Beatles.GetEnumerator);
TStringsFiller.Fill(ListBox.Items, Memo.Lines.GetEnumerator);

Both calls in the above code looks the same, even though the first supplies an object list and the second supplies a TStrings.

And we could clean it up even more by using the RTTI to check for a public GetEnumerator method that returns a class with Current and MoveNext. That way, the call to GetEnumerator in the two lines above could go away as well.

When the Delphi 2010 beta bloggers first started showing off the enhanced RTTI functionality, responses seemed to be split between “Great!” and “But what is it good for?” This post shows one of the simpler scenarios where proper RTTI use can significantly simplify your code.

I have another post coming up that shows a different use of RTTI that can literally save you thousands of lines of code and many, many hours of hard labour.

As always, watch this space.

Update: The promised follow-up is now available. Unfortunately, deadlines, exams and two separate bouts of flu conspired to make it over a month late, but I think you’ll like it.

Advertisement

Leave a Comment

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.

Comments (3)