Abusing extended RTTI for fun and profit
I’ll admit, I don’t like the default settings for Delphi 2010’s extended RTTI. Making almost everything included by default ends up compiling a ton of junk into the EXE, most of which will never get used. But every once in a while, you can find some sort of use for it.
For example, let’s take the common case of trying to create a custom control that’s mostly like an existing VCL control, but slightly different. You need to change some minor feature in some way. Unfortunately, that feature relies on a private field in the base class that the author didn’t have the foresight to make Protected, and so to make it work, you end up having to copy and paste half the original class into your descendant. I’ve seen a few examples of “custom components” incorporate pretty much the entire base class, because they had to in order to make one change. With RTTI generated for private fields in Delphi 2010, we can remedy this oversight, to a certain degree at least.
One thing that’s always bugged me is the behavior of TDBComboBox. It’s not all that often that you have a list of strings that you want to be able to choose from to put into a TStringField. More common, in my experience at least, is to want to use it like a radio group: fill it with strings and let the “important” property be ItemIndex, not Text. (Though, now that I look at it, TDBRadioGroup does the same thing: it acts like what you care about is the name of the radio button, not its ItemIndex.) So far, the only way to use data-aware controls to represent an enumerated type or similar selection by index of a group of strings is to set up a second dataset and use a lookup combo box. I dunno about you, but that always felt like a hack to me. I asked on StackOverflow, more than a year ago, if there was a control that would do what I’m looking for, and never got a good answer, so I decided to try and build my own. Let’s see how we can do this, and how using extended RTTI makes the fix easier.
We start by creating a descendant of TDBComboBox. I’ll call it TDBIndexComboBox. TDBComboBox already does almost everything we need. All that’s really significant is the ItemIndex requirement. We want the data-aware property of the control to be “aware of” the ItemIndex property instead of the Text property. So first we need to know how it handles data-awareness. Let’s look at the constructor for our first clue:
[code lang="delphi"] //inside the constructor FDataLink := TFieldDataLink.Create; FDataLink.Control := Self; FDataLink.OnDataChange := DataChange; FDataLink.OnUpdateData := UpdateData; FDataLink.OnEditingChange := EditingChange; [/code]
It uses a TFieldDataLink object, declared in DBCtrls.pas, and assigns some TDBComboBox methods as event handlers. If we want to change the way the data-aware control works, we’re going to have to override those event handlers in our descendant class. Problem is, the FDataLink field is private, and there’s no way to get at it. TDBRadioGroup and TDBLookupControl offer a protected “property DataLink: TFieldDataLink read FDataLink;”, but the rest of them don’t, for whatever reason. So our first task is getting at FDataLink.
T0 use extended RTTI, you need a TRttiContext. It’s an interface to a global RTTI object pool, and you only need one of them. So it makes sense to attach it at the class level instead of to each individual object. So as long as we’re doing this the Delphi 2010 way, let’s put it in a class constructor.
[code lang="delphi"] type TDBIndexComboBox = class(TDBComboBox) private class var FContext: TRttiContext; FDatalinkField: TRttiField; class constructor Create; class destructor Destroy; ... class constructor TDBIndexComboBox.Create; var classType: TRttiType; begin FContext := TRttiContext.Create; classType := FContext.GetType(ClassParent.ClassInfo); assert(assigned(classType)); FDatalinkField := classType.GetField('FDataLink'); assert(assigned(FDatalinkField)); end; class destructor TDBIndexComboBox.Destroy; begin FContext.Free; end; [/code]
This is pretty straightforward. Create the RTTI Context, retrieve the RTTI info for the parent class, where FDatalink is declared as a private field and retrieve an RTTI reference to FDatalink. I threw in a few assertions for safety checks. If we happen to be running on a codebase where extended RTTI for private fields has been turned off somehow, this will cause hard-to-track-down access violations when we try to use the component.
Next, we need to actually use the datalink, now that we have an RTTI reference to it. So let’s define the constructor.
[code lang="delphi"] //Class definition, continued from above private FDataLink: TFieldDataLink; procedure SetComboText(const Value: string); function GetComboText: string; procedure UpdateData(Sender: TObject); procedure DataChange(Sender: TObject); protected property DataLink: TFieldDataLink read FDataLink; public constructor Create(AOwner: TComponent); override; published property Style default csDropDownList; end; ... constructor TDBIndexComboBox.Create(AOwner: TComponent); begin inherited Create(AOwner); FDataLink := FDatalinkField.GetValue(self).AsObject as TFieldDataLink; FDataLink.OnDataChange := DataChange; FDataLink.OnUpdateData := UpdateData; self.Style := csDropDownList; end; [/code]
I could have used the datalink field object directly throughout the component, but since I need it in more than one method, I decided to redeclare it as a field for simplicity’s sake, and make a protected property for it so it can be extended easily in the future. So the constructor gets the value of the FDataLink defined in the parent class with its somewhat odd “GetValue(self)” syntax. This takes the RTTI field object, which understands members of a class, and gives it an instance (self) to retrieve the value from. After that, we can add new event handlers for OnDataChange and OnUpdateData. I also redefined the default Style property because if you have a combo box with a predefined list of options that the user is supposed to choose from, sticking an edit control onto it is just asking for trouble from the user-interface perspective.
Now that we’ve overcome the difficulty of getting at the datalink, how do we set up the data-aware properties of the control? Sort of like this:
[code lang="delphi"] procedure TDBIndexComboBox.DataChange(Sender: TObject); begin if not (Style = csSimple) and DroppedDown then Exit; if (FDataLink.Field <> nil) and (FDataLink.Field is TNumericField) then self.ItemIndex := FDataLink.Field.AsInteger else if csDesigning in ComponentState then SetComboText(Name) else SetComboText(''); end; procedure TDBIndexComboBox.UpdateData(Sender: TObject); begin if FDatalink.Editing then FDataLink.Field.AsInteger := self.ItemIndex; end; [/code]
DataChange gets called when the data field changes, to update the control. This is mainly a copy of the original TDBComboBox.DataChange, except for the second and third lines of the method. First, we want to restrict it to only numeric fields, since ItemIndex doesn’t make much sense as a string or a boolean. Using a non-numeric field will cause the control to appear inactive. Then, instead of the original “SetComboText(FDataLink.Field.Text)”, we want to change the item index instead. The UpdateData is similarly straightforward. No type-checking here, though if you want it it shouldn’t be too difficult to put some in.
Unfortunately, the SetComboText method is also private, and private methods, unlike private fields, don’t get RTTI generated for them. So I ended up having to copy SetComboText (and GetComboText, which it references) verbatim from the parent class. The way these two are set up, they look like they should be the read and write specifiers for a protected (or even public) ComboText property, but they aren’t. Even so, that’s a lot better than what I’d have had to do if there was no way to get at the FDataLink field. The workarounds required for that would probably involve reimplementing the entire class, or at least a sizable fraction thereof.
I’m using this control in the TURBU project, and I’ll update the repository soon (probably tomorrow sometime) with the code for this. I’ll also put it up as a standalone download here on TURBU Tech tomorrow. What I’ve published here, though, should be enough to get the component working if you have D2010 and some knowledge of the fundamentals of custom component work.
Like it? Hate it? Think of some way I could improve on it? Let me know in the comments!
Very nice example on how to use the new RTTI to grab a private field.
Now, I’m not sure that I like this much better than Hallvard’s hack to get at the private fields through the VMT. It’s cleaner but still an “anti-OOP” hack.
That being said, it surely beats copying and pasting a huge amount of code…
But, hey! I like my private things to stay private 😉
[…] to Delphi 2010, I wouldn’t have been able to do this. But now, there’s a way to fix this sort of problem with extended RTTI. The solution looks like this. (This solution works specifically for TClientDataset, but you can […]
[…] […]