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);
  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');
  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');
  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.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: