1

I have a dictionary with multiple types of TObject (for example, TEdit, TComboBox, TButton, etc).

I want to loop through each of them, and do something with the casted version of each control, but I don't want to check the type and cast for each one, because if I have 10 different controls, I have to do 10 different if checks. Here's my code right now:

for LInputItem in FInputFactory do
begin
  if(LInputItem.Value.ClassName = 'TEdit') then begin
    (TEdit(LInputItem.Value)).Whatever;
  end else if LInputItem.Value.ClassName = 'TComboBox' then begin
    (TComboBox(LInputItem.Value)).Whatever;
  end;
end;

And that's just 2 types.

Fabrizio
  • 7,603
  • 6
  • 44
  • 104
nick
  • 2,819
  • 5
  • 33
  • 69
  • 4
    Not an answer to your question, but `LInputItem.Value.ClassName = 'TEdit'` should be `LInputItem.Value is TEdit`. – Andreas Rejbrand May 14 '19 at 13:49
  • @AndreasRejbrand noted! – nick May 14 '19 at 13:50
  • 1
    @AndreasRejbrand Depends on what he is trying to achieve. But then, even if he specifically want to exclude descendant of TEdit, I'd probably rather go for `LInputItem.Value.ClassType = TEdit`. I've seen class name collisions before. – Ken Bourassa May 14 '19 at 13:52
  • Anyhow, in general, a `TObject` doesn't have a `Whatever` method. Are you sure that all members in your collection do support the thing you want to do on them? If so, then probably they all descend from a given ancestor, and you should declare your dictionary accordingly. For instance, if the values in your dictionary are of type `TAnimal`, which has a (possibly virtual) `MakeNoise` method, you don't need to any casting at all! (And of course, `TDog.MakeNoise` might not do the same thing as `TCat.MakeNoise`.) [In your particular case, a suitable common ancestor might be `TWinControl`.] – Andreas Rejbrand May 14 '19 at 13:53
  • @KenBourassa: I totally agree. – Andreas Rejbrand May 14 '19 at 13:53
  • @AndreasRejbrand actually they are DB aware controls, but they don't descend from the same type. TDBEdit descends from TDBMaskEdit and TDBComboBox descends from TCustomComboBox... – nick May 14 '19 at 13:58
  • Worst case, you might use RTTI to solve your problem. (But I suspect there might be a better solution. It is hard to tell without knowing the exact problem you are facing.) – Andreas Rejbrand May 14 '19 at 13:59
  • @AndreasRejbrand how? I'm pretty new to Delphi coming from a webdev background so this type things are really complicated to me – nick May 14 '19 at 14:00
  • @nick: Well, if you have a collection of objects and they all have a method named `Whatever`, they really should descend from a common ancestor or implement the same interface -- otherwise it is a pure coincidence that two unrelated methods have the same name! So very likely the "correct" solution is to find that common ancestor or interface (and declare the dictionary accordingly). Otherwise, RTTI might be a possible (but ugly and error-prone) way of making this work. But I really cannot give you any more advice without knowing the *exact* problem your are having. – Andreas Rejbrand May 14 '19 at 14:09
  • I'd rather stay away from RTTI. The two types I'm having trouble right now are `TDBEdit` and `TDBComboBox`. Both of them have a `DataSource` property but they descend from different classes. That's my exact problem, I need to set that property... – nick May 14 '19 at 14:18
  • Not sure if duplicate, but related: https://stackoverflow.com/questions/1083087/cast-tobject-using-his-classtype – kobik May 14 '19 at 14:27
  • 1
    In general, I consider a DataSource part of the view, and therefore like to keep it on the form. That also allows you to link it to your controls at design time. Then you only have to make the one link in code between the DataSource and the DataSet or otherwise external resource it gets its information from. Especially because on more complicated forms you might have multiple data sources, and then it becomes harder to do it automagically like you're doing now. – GolezTrol May 14 '19 at 15:02
  • That said, linking controls and data sources from code, I would wrap it in a nice function (procedure) that just accepts a TObject and a TDataSource. In that function, I would probably try RTTI for the fun of it, or just stick with the `if`s and don't worry about it, because it's only that one function anyway. – GolezTrol May 14 '19 at 15:04
  • I just looked in some of the data aware controls. It looks like they don't save the reference to the data source directly, but instead store it in their TDataLink. In addition, each of them implements a `CM_GETDATALINK` message handler that returns that data link. You could cheat, send that message to the controls and set the data source in the data link you get. But it feels like abusing internals (as would RTTI, maybe), so I would still wrap it in the function I mentioned above so I could easily change my implementation. – GolezTrol May 14 '19 at 15:10
  • 2
    @GolezTrol Setting the datasource through the Datalink would break the `FreeNotification` behavior that is implemented by the Datasource setter of the TDBCtrls. So, I wouldn't advise that. – Ken Bourassa May 14 '19 at 15:18
  • @KenBourassa I overlooked that. Well, you could call that yourself as well, but indeed this was already a poor way, and your finding made it worse. – GolezTrol May 14 '19 at 15:27
  • @GolezTrol, it is not cheating - the CM_GETDATALINK message has been invented for exactly that case and is used by the VCL itself for a similar purpose. – Uwe Raabe May 14 '19 at 18:47

2 Answers2

3

There is no magic recipe here. You need to, one way or another, determine if the object is of a class that has a Datasource property.

Your method is one way to do it. Though, as stated in comments by Andreas, you should really call LInputItem.Value is TDBWhatever instead of testing for class name.

Another way to do it would be through RTTI.

  (Only light error checking included)
  vRTTIContext := TRTTIContext.Create;
  vRTTIField := vRTTIContext.GetType(LInputItem.Value.ClassType).GetProperty('Datasource');
  if Assigned(vRTTIField) then
    vRTTIField.SetValue(LInputItem.Value, SomeDatasource);

But you seem to reject RTTI solutions. So at this point, short of exotic, hackish solutions, the only one that comes to mind would be have a different dictionary for each of your DBCtrl types.

There's plenty of other "solutions", but none really good or advisable (That I can think of at the moment), not in a vacuum at least. (Using interface, using wrapper classes, there's probably some messy way to do it with anonymous method too...).

But, as GolezTrol mentionned in comment and I strongly agree with, what you usually want to do is not to replace the Datasource property of your controls, but replacing the Dataset property of your datasource.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Ken Bourassa
  • 6,363
  • 1
  • 19
  • 28
  • I ended up using the RTTI solution. Does it have any disadvantage? – nick May 24 '19 at 18:00
  • The main disadvantage of using RTTI is performance. But in your current use case (I'd be seriously surprised if you had hundreds of thousands of control to update), I don't believe you would see a meaningful difference compared to using different dictionary per control types. *IF* performance is lacking, you could always cache the value of vRTTIField which would boost performance. But I'd call that premature optimization at this point. – Ken Bourassa May 25 '19 at 14:45
-4

An old trick to get strings to map into a case statement might help here as well.

   var I :integer  := IndexText('Red',['Green','Blue','Red','Yellow']);
   case I of
      0: // Green
         ;
      1: // Blue
         ;
      2: // Red
         ;
      3: // Yellow
         ;
   end;

Yes this doesn't match the specific code in the question: instead it shows a method to convert a string from a list of strings into a case statement. An abstraction likely easily converted by the poster to replace the series of ifs.

Brian
  • 6,717
  • 2
  • 23
  • 31
  • 1
    This could work if you replace `Red` by the `LInputItem.Value.ClassName`, and populate the array with all the class names. But it's not even clear if you intend it that way, it's not less code than a simple `if`, you still have to do the typecasts, it's harder to maintain, and it doesn't let you benefit of the `is` operator, which could reduce the list of classes to process. – GolezTrol May 14 '19 at 19:16
  • @GolezTrol I tried to show a method to do a string test against multiple other strings as a case statement. More abstract than straight coding the answer but more widely applicable. – Brian May 14 '19 at 20:10