Search code examples
delphirttidelphi-10.3-rio

How to assign event handler to event property using RTTI?


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;

Solution

  • 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;