5

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;
4
  • I think DSharp has some extra functionality to make handling anonymous functions, but I was hoping I could avoid using an external library just for this Commented Sep 23, 2014 at 8:01
  • Why would you declare Initialise like that? It seems like it would have been more expressive to just declare it without generics: Initialise(MainForm: IFrmMainFormInterface). Of course, that still wouldn't allow you to assign the interface member to a standard method pointer, but I'm puzzled why you had the complex starting point in the first place. Commented Sep 24, 2014 at 23:27
  • I'm experimenting with a MVVM design pattern. In the initialisation I bind the form to a ViewModel. binding.Bind<T>(MainFormViewModel, 'ScheduledWorkRecords'); MainForm.WorkRecords := binding; For testing purposes I also have a mock user interface object which can be used in place of the actual form. Commented Sep 26, 2014 at 1:29
  • The class that I'm using for binding needs a generic class type rather than an interface to bind to (ie: I can use binding.Bind<IFrmMainFormInterface>(...)). Commented Sep 26, 2014 at 1:33

3 Answers 3

5
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.

Sign up to request clarification or add additional context in comments.

Comments

2

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;

Comments

1

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

3 Comments

I've tried this both ways, it does not make a difference. It seems delphi does not support doing things this way.
I should probably also add that the MainForm variable is declared in a function as: procedure TController.Initialise<T>(MainForm : T);
@sav, Ok, that changes the problem quite a bit.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.