views:

587

answers:

5

I have an Application layout that is based on a treeView at the left, and a panel on the right. The panel hosts a differnt TForm class depending on the tree node selected (a kind of 'form explorer'). There is only one form displayed at a time which exposes underlying data stored elsewhere and the form instance is created and destroyed on each new tree node click.

This all works fine except for the following scenario. Click a button on the form which launches an action that takes a second or so. During this action there may be a call to Application.ProcessMessages. Now just before this action has actually completed, the User clicks a new tree node. This wmMousedown message is processed causing the form to be freed immeditely. The action code then returns to the form code to find that self has changed and causes an AV.

My question is, is there a way to know that the form's messages have all been processed and completed before I allow the form to be freed? Modal forms seem to do this when the close button is clicked because they pause before closing if busy...

Thanks Brian

+1  A: 

Implement IsBusy property in every placed form's class (use inheritance - implement this property in parent form). Before removing a form from hosting panel (no matter if by calling Free or simply moving from panel) check if it’s IsBusy is not true. If your hosted form is busy, wait until it finish, and then remove it. You may even add some way to notify the hosted form, that it should abort it’s long running task.

Not only will it help with your current problem, but also you will be able to sanitize some business logic inside your forms.

So form-changing code in your TreeView should have following code inside:

    {FCurrentForm is a reference to currently placed form on panel}
    if (FCurrentForm.IsBusy) do
    begin
      {remember some information that will be used to create new form}
      FNewFormToBeAdded := ... 
    end
    else
    begin
      FreeCurrentForm();
      PlaceNewFormOnPanel();
    end;

Therefore you should have some routine like:

   procedure THostForm.NotifyMeAboutTaskFinished;
   begin
     if FNewFormToBeAdded <> 0 than 
     begin
       FreeCurrentForm();
       PlaceNewFormOnPanel();
     end; 
   end;

And in your HOSTED form you can have

procedure TSomeHostedForm.btnDoLongTaskClick(Sender : TObject);
begin
  IsBusy := true;
  try
    {... do some tikme taking task ...}
  finally
    IsBusy :=false;
    NotifyHostingFormIAMNotBusyAnymore();
  end;
end;
smok1
Your example code doesn't make much sense. Either the code is executed in the `IsBusy()` method, then the method has the wrong name, and the `Sleep()` needs to be removed. Or the code is executed in another method, then it's not called at all.
mghie
I am thinking of using this code in TreeView.OnClick (or OnMouseDown) event. When user clicks new node on a TreeVew , we should check if we can call Free on FCurrentForm. If FCurrentForm is actually busy, we should wait until FCurrentForm finishes his job, than Free FCurrentForm, create new form (appropriate to selection), and set FCurrentForm to point to newely created form. So I think of simply NOT changing current form if current form is busy.
smok1
Where in your code is `FCurrentForm` going to finish its job?
mghie
You have to alter your forms (every one form) to support this property. So, imagine you have a placed form that does sth in btnDoLongTaskClick event. When you start this event procedure, you set up this form's IsBusy to true, and when ending this procedure you set IsBusy to false (it is good to false this property on finally). Hope this is clear - if not, I will post longer code piece.
smok1
OK, I'll try to explain better: If `FCurrentForm.IsBusy` does only check whether the form is busy, but does no real work, and you use a `Sleep(500)` in a loop - how can the form ever become not busy? The long-running task will not progress, the busy stays True - you have created an infinite loop. See my first comment: Either `IsBusy()` can reset the busy state, then sleeping is wrong, and the name is misleading. Or `IsBusy()` does only check, then there's an infinite loop.
mghie
@mghie - yes, this can work in current form only if long-running task is done in another thread. I will update this.
smok1
I thought so. However, if the long-running task is in another thread there's no longer any need to call `Application.ProcessMessages` at all, so it's not really relevant in the context of this question. If you use multiple threads there's no need to **poll** until the form is no longer busy, there are better ways of notifying the GUI thread from the worker thread.
mghie
+7  A: 

Avoid using Application.ProcessMessages at all costs!

The most common inappropriate usage I see for this is to allow the GUI to be repainted, e.g. after updating a label caption. The safest way to ensure the GUI is updated is to explicitly repaint any affected controls using the Update method (which bypasses paint messages and causes a control to repaint directly). Or it may be the Refresh method - or it may be either or both! Sad to say I can never remember off the top of my head!

Application.ProcessMessages causes "re-entrancy" problems as you've discovered, effectively creating potential new, short lived, "main message loops" within your code that can lead to difficult to diagnose and hard to reproduce problems.

I would examine the use of Application.ProcessMessages in this case and see if an alternative approach could not be devised to eliminate it's use from your code, rather than trying to patch up lots of other code simply to make it cope with the problems that Application.ProcessMessages causes.

NOTE: One exception to the rule is that using Application.ProcessMessages to maintain responsiveness in a "Cancel" button on a "progress" message/dialog box is relatively safe as long as that progress message/dialog box is modal and the rest of your application is effectively disabled whilst that dialog is presented, so that only that "Cancel" button could possible respond to any messages

Deltics
Useul comments thanks. Brian
Brian Frost
+4  A: 

To answer the actual question in the final paragraph... :)

If you call Release on a form this posts a message to the form which will result in it Free'ing itself when it receives that message.

Since the message is posted to the message queue, it will arrive only after any/all other current messages for that form have been processed, neatly achieving exactly what you ask for I think.

Deltics
A: 
Mark Elder
A: 

While I agree with both of "Deltics" answers, there is another option - depending on whether you need to free the form.

On the form FormClose event set the Action to caHide. This will hide rather than destroying the form. You will need to keep track of what forms you have assigned (probably using the "Data" pointer in the TTreeNode).

Gerry
Not a nice solution because the form's controls and timers are still running. My architecture is to use forms only for user access to underlying data. Where forms and controls are converned "If you cant see it, it's not there".
Brian Frost