Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing a function instead of calling it (Delphi)

So I have a form in Delphi

TFrmMainForm = class(TForm, IFrmMainFormInterface)
  public
    procedure Display(Sender:TObject); 
end;

The interface is

IFrmMainFormInterface = interface
  procedure Display(Sender:TObject);
end;

And another class

TMainFormViewModel = class
    strict private
      fTimer : TTimer;
      function GetOnTimer : TNotifyEvent;
      procedure SetOnTimer(timerEvent : TNotifyEvent);
    public
      property OnTimer : TNotifyEvent read GetOnTimer write SetOnTimer;
end;

implementation

function TMainFormViewModel.GetOnTimer : TNotifyEvent;
begin
    Result := fTimer.OnTimer;
end;

procedure TMainFormViewModel.SetOnTimer(timerEvent : TNotifyEvent);
begin
    fTimer.OnTimer := timerEvent;
end;

I have an instance of the Form MainForm and the view model class MainFormViewModel

and I want to try

MainFormViewModel.OnTimer := IFrmMainFormInterface(MainForm).Display

The problem is this give me an error message

Not enough actual parameters

I belive this is because delphi is trying to call the display function rather than assign it to the OnTimer event. I'm not sure how to fix this, I've tried using the @ operator with no success.

EDIT

I should add that the MainForm is declared in this function as

procedure Initialise<T:Class, IFrmMainFormInterface>(MainForm : T);

procedure TController.Initialise<T>(MainForm : T);
begin
    MainFormViewModel.OnTimer := IFrmMainFormInterface(MainForm).Display ;
end;
like image 559
sav Avatar asked Sep 23 '14 08:09

sav


3 Answers

MainFormViewModel.OnTimer := IFrmMainFormInterface(MainForm).Display;

The problem is that you cannot use a method of an interface in this context. The OnTimer event is an of object method type. It must be a method of an object or record.

like image 156
David Heffernan Avatar answered Oct 20 '22 18:10

David Heffernan


The problem is that interface method references are not compatible with object method references.

But instead of directly passing a reference to a method on some interface implementation, simply pass the interface reference itself. On the other side of the fence, instead of keeping a reference to a specific method to be called, you hold the reference to the interface that implements that method.

You then simply call the target method at the appropriate time.

It is in fact crucial that you do this in a reference counted environment, since a reference to a specific method on an interface will NOT contribute to the reference count on the interface itself.... if you contrive to maintain only a reference to some method then by the time your code tries to call that method, the implementation object might have been destroyed (since you were not maintaining any reference to it).

If you need to refer to any aspect of some object that implements an interface, then - in a reference counted context - you must maintain a reference to that interface.

Additionally however, I would suggest a separation of concerns. i.e. separating the fact that your form responds to a timer event from its ability to be displayed:

IfrmMainFormInterface = interface
[..guid..]
  procedure Display;
end;


ITimerListener = interface
[..guid..]
  procedure OnTimer(Sender: TObject);
end;


TMainFormViewModel = class
    strict private
      fTimer : TTimer;
      fOnTimer: ITimerListener;
      procedure DoOnTimer(Sender: TObject);  // internal event handler for fTimer.OnTimer
    public
      property OnTimer: ITimerListener read fOnTimer write fOnTimer;  // no need for getter/setter anymore
end;


procedure TMainFormViewModel.DoOnTimer(Sender: TObject);
begin
  // Enabling/disabling the timer might not be needed/or appropriate, but if it is
  //  then you can take care of that here, rather than relying on the listener to
  //  do it

  fTimer.Enabled := FALSE;
  try
    if Assigned(fOnTimer) then
       fOnTimer.OnTimer(self);  // call the method on the assigned listener interface

  finally
    fTimer.Enabled := TRUE;
  end;
end;



// Meanwhile, Somewhere in your view model initialisation....

fTimer.OnTimer := DoOnTimer;

Then, in your TMainForm implementation:

TMainForm = class(TForm, IfrmMainFormInterface,
                         ITimerListener)
..
  procedure Display;
  procedure OnTimer(Sender: TObject);
..
end;


procedure TMainForm.OnTimer(Sender: TObject);
begin
  if Sender is TMainFormViewModel then
    Display;
end;

Which you "attach" to the view model timer using the interface type property by directly assigning the main form itself (which will result in an interface reference of the correct type being passed):

ViewModel.OnTimer := frmMain;

You might have noticed that in the above example, the view model passes "self" as the Sender of the OnTimer call to the listener interface, rather than passing through the originating timer object. This is in order to demonstrate how the listener might use the class type of the Sender to (potentially) discriminate between multiple timer sources that it may be listening to.

There are a number of ways of approaching that problem, if it arises, of which this is only one.

Another would be to take advantage of the fact that you now have a specific interface listener method for this purpose, separate from the specific implementation of the underlying event method type (TNotifyEvent). As a result you can introduce whatever additional parameters are required to your timer listener interface method as suits your needs. e.g. If your view models have multiple timers then your ITimerListener interface might contract that a Timer ID be passed in addition to (or instead of) the Sender, for example:

ITimerListener = interface
[..guid..]
  procedure OnTimer(Sender: TObject; aTimerID: Integer);
end;
like image 2
Deltics Avatar answered Oct 20 '22 20:10

Deltics


I think MainFormViewModel.OnTimer := MainForm.Display should work. Why casting the instance to the Interface anyway ?

like image 1
iamjoosy Avatar answered Oct 20 '22 20:10

iamjoosy