Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delphi: Test event handler assignment

I want to assign an event handler in constructor, if it does not have one assigned. Consequentially I want to remove the eventually assigned event handler in destructor. I wrote the code as follows, but cannot be compiled.

constructor TSomeControl.Create(Panel: TPanel);
begin
  inherited Create;
  FPanel := Panel;
  if not Assigned(FPanel.OnResize) then
    FPanel.OnResize := HandlePanelResize;
end;

destructor TSomeControl.Destroy;
begin
  if @FPanel.OnResize = @HandlePanelResize then // [dcc32 Error] E2036 Variable required
    FPanel.OnResize := nil;
  FPanel := nil;
  inherited;
end;

How to test it properly? I know a solution is to use a variable to record, whether I have assigned OnResize. But I do not want this as solution.

like image 379
stanleyxu2005 Avatar asked Jun 08 '14 16:06

stanleyxu2005


2 Answers

No need to write any custom code here as you can use the already existing comparers from Generics.Defaults:

destructor TSomeControl.Destroy;
begin
  if Assigned(FPanel) and TEqualityComparer<TNotifyEvent>.Default.Equals(
    FPanel.OnResize, HandlePanelResize) then
    FPanel.OnResize := nil;
  FPanel := nil;
  inherited;
end;
like image 122
Stefan Glienke Avatar answered Sep 28 '22 09:09

Stefan Glienke


This is complicated by the fact that OnResize is a property rather than a variable. And it's quite hard to refer to a method directly without the compiler thinking you want to call the method. This is the big drawback of Pascal's convenience of allowing you to call a procedure without using parens.

All this makes it rather hard to do it in a one-liner. As far as I can see you will need to do something like this:

destructor TSomeControl.Destroy;
var
  Method1, Method2: TNotifyEvent;
begin
  if Assigned(FPanel) then
  begin
    Method1 := FPanel.OnResize;
    Method2 := HandlePanelResize;
    if TMethod(Method1) = TMethod(Method2) then
      FPanel.OnResize := nil;
  end;
  FPanel := nil;
  inherited;
end;

This relies on modern Delphi's TMethod record which includes an overloaded equality operator to make the = test work.

I would wrap this all up in a generic method if I was doing it more than once. It might look like this:

type
  TEventComparer = class
    class function Equal<T>(const lhs, rhs: T): Boolean; static;
  end;

class function TEventComparer.Equal<T>(const lhs, rhs: T): Boolean;
begin
  Assert(SizeOf(T)=SizeOf(TMethod));
  Result := TMethod((@lhs)^)=TMethod((@rhs)^);
end;

You'd call it like this:

destructor TSomeControl.Destroy;
begin
  if Assigned(FPanel) and TEventComparer.Equal<TNotifyEvent>(FPanel.OnResize, 
    HandlePanelResize) then
    FPanel.OnResize := nil;
  FPanel := nil;
  inherited;
end;

One thing that this highlights is that the generic constraints available to you do not allow you to constraint a type to being a method pointer. Hence the basic sanity check that the size of T is the same as the size of a method. This doesn't offer much safety though. You can call this method passing Int64, or Double. I'd be interested to see if anyone can come up with a cleaner variant.

like image 20
David Heffernan Avatar answered Sep 28 '22 08:09

David Heffernan