views:

346

answers:

5

How do you prevent a new event handling to start when an event handling is already running?

I press a button1 and event handler start e.g. slow printing job. There are several controls in form buttons, edits, combos and I want that a new event allowed only after running handler is finnished.

I have used fRunning variable to lock handler in shared event handler. Is there more clever way to handle this?

procedure TFormFoo.Button_Click(Sender: TObject);    
begin
  if not fRunning then
  try
    fRunning := true;
    if (Sender = Button1) then // Call something slow ...
    if (Sender = Button2) then // Call something ...
    if (Sender = Button3) then // Call something ...
  finally
    fRunning := false;
  end;
end;
+2  A: 

You don't have to do this at all, since all of this is happening in the main (VCL) thread: No other button (VCL) event can be entered until the previous (VCL) event handler has returned... The simultaneous execution of another event handler could only happen unexpectedly if some other thread was preemptively entering a second button event (before the first one has completed), but that can't happen, since there is only one VCL thread.

Now if the lengthy thing you are doing is done in another thread because you don't want it to block the GUI, then you can simply set the Button.Enabled property to false until your processing is done.
And if you decide to just stick in the button event until everything has completed, use application.processmessages frequently enough in your processing loop to prevent the gui from freezing. In which case, yes, you must disable the original button to prevent reentry.

filofel
Sorry, that is not true.
pKarelian
Then I'd be interested to be explained here how the VCL can be executing two button events at the same time in a single thread context.
filofel
Not true. Events can be nested and recursion can easily occur especially when Application.ProcessMessages is called within event code (as we often have to do).
Brian Frost
But then, is is programmer controlled and predictable, rather than unexpected and preemptive, since you explicitly then tell the VCL "reenter the Win message pump". That is, you only reenter an event if you explicitly programmed it. The default situation is that a VCL event is not reentered. Plus, I mentioned that above anyway.
filofel
@BrianFrost: Neither do we have to call Application.ProcessMessages, nor indeed should we. That is a terrible strategy for creating the perception of multi-threadedness. If you have a long-running task, and you need GUI events to run during that long running task, you should be using a thread, just like everyone else. Calling Application.ProcessMessages inside an event handler is *begging* for trouble.
cjrh
Let's show hands who has never used Application.ProcessMessages... <g>
Lieven
I used to use Application.ProcessMessages. I learned my lesson though and stopped using it years ago. I *never* use it now.
Deltics
+1  A: 

If your app is a single-threaded one, then while your event-handler code is running, your app cannot run other codes, so all calls to that event-handler will be serialized, and you don't need to be worried.

If your event-handler is running any asynchronous job, then you can use the technique you presented in your question.

vcldeveloper
Unless that code calls Application.ProcessMessages. In which case the vent-handler can be called twice.
Lars Truijens
+2  A: 

Your solution is OK. You can also link button clicks to actions and enable/disable actions in TAction.OnUpdate event handler, but you still need fRunning flag to do it. The "if no fRunning" line may be not nessesary here, but I don't removed it because it is more safe:

// Button1.Action = acButton1, Button2.Action = acButton2, etc

procedure TForm1.acButtonExecute(Sender: TObject);
begin
  if not fRunning then

  try
    fRunning:= True;
    if (Sender = acButton1) then // Call something slow ...
    if (Sender = acButton2) then // Call something ...
    if (Sender = acButton3) then // Call something ...
  finally
    fRunning:= False;
  end;

end;

procedure TForm1.acButtonUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled:= not fRunning;
end;
Serg
Another method is simply to set Enabled := False at form level. This is obviously best with a try...finally handler
Gerry
Thanks Serg. I'll try TActionList solution.
pKarelian
+5  A: 

Another option (that does not require a flag field) would be to temporarily assign NIL to the event:

procedure TForm1.Button1Click(Sender: TObject);
var
  OldHandler: TNotifyEvent;
begin
  OldHandler := (Sender as TButton).OnClick;
  (Sender as TButton).OnClick := nil;
  try
    ...
  finally
    (Sender as TButton).OnClick := OldHandler;
  end;
end;

For convenience sake this could be wrapped into an interface:

interface

function TempUnassignOnClick(_Btn: TButton): IInterface;

implementation

type
  TTempUnassignOnClick = class(TInterfacedObject, IInterface)
  private
    FOldEvent: TNotifyEvent;
    FBtn: TButton;
  public
    constructor Create(_Btn: TButton);
    destructor Destroy; override;
  end;

constructor TTempUnassignOnClick.Create(_Btn: TButton);
begin
  Assert(Assigned(_Btn), 'Btn must be assigned');

  inherited Create;
  FBtn := _Btn;
  FOldEvent := FBtn.OnClick;
  FBtn.OnClick := NIL;
end;

destructor TTempUnassignOnClick.Destroy;
begin
  FBtn.OnClick := FOldEvent;
  inherited;
end;

function TempUnassignOnClick(_Btn: TButton): IInterface;
begin
  Result := TTempUnassignOnClick(_Btn);
end;

to be used like this:

procedure TForm1.Button1Click(Sender: TObject);
begin
   TempUnassignOnClick(Sender as TButton);
   ...
end;
dummzeuch
This nice solution if there is only one Button1 in form. Button1's OnClick is disabled, but Button2 can trigger a new OnClick event if Application.ProcessMessages is called during Button1 event handling.
pKarelian
Thanks dummzeuch. Interface wrapper is very handy way to destroy temporary event object. You don't have to call free().
pKarelian
+1 but in *theory*, there's a possibility that the event doesn't get re-attached for some time. In practice, I think it is safe to assume that the interface instance gets destroyed when the call is done (and the eventhandler is re-attached).
Lieven
@pKarelian - you could pass the *TFormFoo.Button_Click* handler. It is then equivalent (but cleaner) with the fRunning variable solution.
Lieven
@pKarelian: I was assuming that the code in the question was only structured that way in order to not require mutliple flag fields. But you are right, I should not have taken that for granted.
dummzeuch
@Lieven: The interface object usually gets destroyed when it is assigned to NIL or at the end of the method in which it was created.
dummzeuch
@dummzeuch: true but although it is very doubtfull it will change, it relies on an implementation detail that CodeGear is free to change at any time.
Lieven
+2  A: 

As Gerry already mentioned in one of the comments, you can disable entire form:

procedure TFormFoo.Button_Click(Sender: TObject);    
begin
  try
    Enabled := False;
    //...
  finally
    Enabled := True;
  end;
end;
Torbins
Thank you Torbins. Nice and simple solution.
pKarelian