3

I have a class that has a few event properties, and another class that contains the event handlers. At compile-time I don't know the structure of either class, at run-time I only get to know the match between event property and event handler using their names. Using RTTI, I'd like to assign the event handlers to the respective event properties, how can I do so?

I currently have something like this:

type
  TMyEvent = reference to procedure(const AInput: TArray<string>; out AOutput: TArray<string>);
  TMyBeforeEvent = reference to procedure(const AInput: TArray<string>; out AOutput: TArray<string>; out ACanContinue: boolean);

  TMyClass = class
  private
    FOnBeforeEvent: TMyBeforeEvent;
    FOnEvent: TMyEvent;
  public
    property OnBeforeEvent: TMyBeforeEvent read FOnBeforeEvent write FOnBeforeEvent;
    property OnEvent: TMyEvent read FOnEvent write FOnEvent;
  end;

  TMyEventHandler = class
  public
    procedure DoBeforeEvent(const AInput: TArray<string>; out AOutput: TArray<string>; out ACanContinue: boolean);
    procedure DoEvent(const AInput: TArray<string>; out AOutput: TArray<string>);
  end;

  procedure AssignEvent;

implementation

uses
  Vcl.Dialogs, System.RTTI;

{ TMyEventHandler }

procedure TMyEventHandler.DoBeforeEvent(const AInput: TArray<string>;
  out AOutput: TArray<string>; out ACanContinue: boolean);
begin
  // do something...
end;

procedure TMyEventHandler.DoEvent(const AInput: TArray<string>;
  out AOutput: TArray<string>);
begin
  // do something...
end;

procedure AssignEvent;
var
  LObj: TMyClass;
  LEventHandlerObj: TMyEventHandler;
  LContextObj, LContextHandler: TRttiContext;
  LTypeObj, LTypeHandler: TRttiType;
  LEventProp: TRttiProperty;
  LMethod: TRttiMethod;
  LNewEvent: TValue;
begin
  LObj := TMyClass.Create;
  LEventHandlerObj := TMyEventHandler.Create;

  LContextObj := TRttiContext.Create;
  LTypeObj := LContextObj.GetType(LObj.ClassType);
  LEventProp := LTypeObj.GetProperty('OnBeforeEvent');

  LContextHandler := TRttiContext.Create;
  LTypeHandler := LContextHandler.GetType(LEventHandlerObj.ClassType);
  LMethod := LTypeHandler.GetMethod('DoBeforeEvent');

  LEventProp.SetValue(LObj, LNewEvent {--> what value should LNewEvent have?});
end;

2 Answers2

6

A reference to procedure(...) is an anonymous method type. Under the hood, it is implemented as an interfaced object (ie, a class that implements the IInterface interface) with an Invoke() method that matches the parameters of the procedure.

So, can't use TMyEventHandler.DoBeforeEvent() directly with TMyClass.OnBeforeEvent, for instance, since TMyEventHandler does not match that criteria. But, you can wrap the call to DoBeforeEvent() inside of an actual anonymous procedure, eg:

procedure AssignEvent;
var
  LObj: TMyClass;
  LEventHandlerObj: TMyEventHandler;
  LEventHandler: TMyBeforeEvent;
  LContextObj: TRttiContext;
  LMethod: TRttiMethod;
  LEventProp: TRttiProperty;
begin
  LObj := TMyClass.Create;
  LEventHandlerObj := TMyEventHandler.Create;

  LContextObj := TRttiContext.Create;
  LMethod := LContextObj.GetType(LEventHandlerObj.ClassType).GetMethod('DoBeforeEvent');

  LEventHandler := procedure(const AInput: TArray<string>; out AOutput: TArray<string>; out ACanContinue: boolean);
  begin
    // Note: I don't know if/how TRttiMethod.Invoke() can handle
    // 'out' parameters, so this MAY require further tweaking...
    LMethod.Invoke(LEventHandlerObj, [AInput, AOutput, ACanContinue]);
  end;

  LEventProp := LContextObj.GetType(LObj.ClassType).GetProperty('OnBeforeEvent');
  LEventProp.SetValue(LObj, TValue.From(LEventHandler));
end;

Same with using TMyEventHandler.DoEvent() with TMyClass.OnEvent.


Alternatively, if you change the reference to procedure(...) into procedure(...) of object instead, you can then use the TMethod record to assign TMyEventHandler.DoBeforeEvent() directly to TMyClass.OnBeforeEvent, eg:

procedure AssignEvent;
var
  LObj: TMyClass;
  LEventHandlerObj: TMyEventHandler;
  LEventHandler: TMyBeforeEvent;
  LContextObj: TRttiContext;
  LEventProp: TRttiProperty;
  LMethod: TRttiMethod;
begin
  LObj := TMyClass.Create;
  LEventHandlerObj := TMyEventHandler.Create;

  LContextObj := TRttiContext.Create;

  LEventProp := LContextObj.GetType(LObj.ClassType).GetProperty('OnBeforeEvent');
  LMethod := LContextObj.GetType(LEventHandlerObj.ClassType).GetMethod('DoBeforeEvent');

  with TMethod(LEventHandler) do
  begin
    Code := LMethod.CodeAddress;
    Data := LEventHandlerObj;
  end;

  LEventProp.SetValue(LObj, TValue.From(LEventHandler));
end;

Same with using TMyEventHandler.DoEvent() with TMyClass.OnEvent.

Remy Lebeau
  • 454,445
  • 28
  • 366
  • 620
5

Good question!

I believe I have found a sound approach.

To illustrate it, create a new VCL application with a form (Form1) and a single button (Button1) on it. Give the form an OnClick handler:

procedure TForm1.FormClick(Sender: TObject);
begin
  ShowMessage(Self.Caption);
end;

We will attempt to assign this handler to the button's OnClick property using only RTTI and property and method names (as strings).

The key thing to remember is that a method pointer is a pair (code pointer, object pointer), specifically, a TMethod record:

procedure TForm1.FormCreate(Sender: TObject);
var
  C: TRttiContext;
  b, f: TRttiType;
  m: TRttiMethod;
  bp: TRttiProperty;
  handler: TMethod;
begin

  C := TRttiContext.Create;

  f := C.GetType(TForm1);
  m := f.GetMethod('FormClick');

  b := C.GetType(TButton);
  bp := b.GetProperty('OnClick');

  handler.Code := m.CodeAddress;
  handler.Data := Form1;

  bp.SetValue(Button1, TValue.From<TNotifyEvent>(TNotifyEvent(handler)));

end;
Andreas Rejbrand
  • 95,177
  • 8
  • 253
  • 351
  • Why TRttiContext.Create.Create ? – fpiette Oct 01 '20 at 15:15
  • Thank you, it worked! The only thing I had to change in my code was the declaration of `TMyEvent` and `TMyBeforeEvent` from `reference to procedure` to `procedure of object` (in `TValue.From(TMyEvent(handler))` I had an _Invalid typecast_ error from the compiler) – Marina Finetti Oct 01 '20 at 15:24
  • I would suggest changing `handler` to `TNotifyEvent`, and then type-cast when setting its `Code` and `Data`, not when passing it to `TValue`, eg: `var handler: TNotifyEvent; ... TMethod(handler).Code := m.CodeAddress; TMethod(handler).Data := Self; bp.SetValue(Button1, TValue.From(handler));` – Remy Lebeau Oct 01 '20 at 17:13
  • @RemyLebeau: That's purely a matter of taste; personally I don't even think I have a preference. The benefit is that the type is more specific, but in practice you need one more cast (or you can use the highly popular `with` construct or the `absolute` keyword...). – Andreas Rejbrand Oct 01 '20 at 17:16