by Jim Karabatsos - GUI Computing
While working on a Wizard-like project recently, I came across a rather subtle problem when presenting the user with a series of forms modally.
I am sure that you have all seen a Wizard in action somewhere. Essentially, a Wizard consists of a series of dialog boxes that guide you through a complex task by asking you to make a few selections at a time. Typically, the choices you make affect which further choices you are offered. I am not a big fan of Wizard-style interfaces but there is no denying that they can be quite useful for new users or to guide you through a task that you do not perform often enough to be comfortable with it.
There are a number of programming techniques that you can use to implement a Wizard. Access, which uses Wizards extensively, uses multipage forms to good effect. Unfortunately, VB does not offer this facility, although you can emulate it by creating one frame for each Wizard dialog you want and playing with the Visible property of the frames to make it appear that different forms are being shown. Essentially, you are building a tabbed dialog without the tabs.
The problem that I have with this approach is the same problem that I have with the use of Tab controls - resource consumption. The Wizard I was building required around twenty forms (don't ask - you really don't want to know), and placing all the required controls on one form would represent serious problems for those users still using Windows 3.x. Another alternative was to use SetParent to move a frame from a series of forms onto one visible form. This works (quite well, too) but I've had a few people suggest to me that there may be some reliability issues involved here. I have not encountered any problems in the past as long as you are very careful to always put the panel back to its original parent before closing the form. Still, this approach was deemed inappropriate.
What I really wanted to do was to simply chain from one form to another. Indeed, code similar to this works without any problems :
Sub ChainToForm (frmFrom As Form, frmTo As Form) Dim iMousePointer As Integer iMousePointer = Screen.MousePointer Screen.MousePointer = MP_HOURGLASS Load frmTo frmTo.Top = frmFrom.Top frmTo.Left = frmFrom.Left frmTo.Icon = frmFrom.Icon frmTo.Show Unload frmFrom Screen.MousePointer = iMousePointer End SubUsing this code, you can basically go from form to form by passing the 'from' and 'to' forms to this one function. The 'to' form is loaded and placed at the same position as the 'from' form, then the 'to' form is shown before the 'from' form is unloaded. This is done in this order to make a smoother transition from form to form. (All the forms are exactly the same size and are not resizable.)
All was good with the world.
A late change in the program spec (ever had one of those?) required that the Wizard be invoked from a modal form. Amongst other things, this meant that the Wizard's forms also had to be shown modally because it is illegal to show a non-modal form from a modal one. Hmm. Serious head-scratch mode was entered, and the following code was our first attempt :
Sub ChainToForm (frmFrom As Form, frmTo As Form) Dim iMousePointer As Integer iMousePointer = Screen.MousePointer Screen.MousePointer = MP_HOURGLASS Load frmTo frmTo.Top = frmFrom.Top frmTo.Left = frmFrom.Left frmTo.Icon = frmFrom.Icon Unload frmFrom Screen.MousePointer = iMousePointer frmTo.Show MODAL End SubWell, gee, it looked all right to me. By moving the Show to the end of the procedure, we are unloading the 'from' form first, then loading the 'to' form. It sure looks like it will work.
In fact, when we first ran this, it did appear to work. You could get through the Wizard without any problems. It wasn't until we started to put this through some serious pounding that we found out that it wasn't really working at all. What would happen is that the message "Out of Stack Space" would occur if you went backwards and forwards through the Wizard forms a few times.
To understand what was going on, we need to take a step back from this fragment and recall that in VB, all code (except maybe Sub Main) executes as a result of an event being recognised. This is true for code in modules too; after all, you need to call a procedure from somewhere and if you trace the calls back far enough, you will always find an event procedure.
So what was really happening was that the Click event for one of the navigation buttons on a Wizard form would do something like this :
Sub cmdNext_Click()... do some validation and decision making
ChainToForm Me, frmWhatever End SubWhen ChainToForm does an Unload on frmFrom, the unload cannot be actioned immediately because the form has an active event handler. VB basically 'remembers' that the form needs to be unloaded and will unload it when the 'current' event procedure exits.
The problem is that the current event procedure will not exit until ChainToForm returns, and ChainToForm shows the next form modally, so it won't return until that form is hidden or unloaded. But wait, that form will also call ChainToForm to navigate to its next form. I think you see the problem here. We have recursive calls to ChainToForm happening on each change of form, and the recursion does not unwind until the last form unloads itself, at which time all the pending unloads are also actioned in reverse order.
The sad thing is that I did know about the way that VB defers unloading until the event terminates; it's just that I didn't grok that it applied in this situation.
So, how do you have one modal form chain to another and allow this to continue indefinitely without blowing your stack? The short answer is that you don't. Instead, you write a procedure that loops around, displaying one modal form after another until the work is done. Here's our implementation :
Global gfrmNextWizard As Form Sub ProcessWizard() Dim frm As Form Do While Not (gfrmNextWizard Is Nothing) Set frm = gfrmNextWizard Set gfrmNextWizard = Nothing frm.Show MODAL Loop End Sub Sub ChainToForm (frmFrom As Form, frmTo As Form) Dim iMousePointer As Integer iMousePointer = Screen.MousePointer Screen.MousePointer = MP_HOURGLASS Load frmTo frmTo.Top = frmFrom.Top frmTo.Left = frmFrom.Left frmTo.Icon = frmFrom.Icon Set gfrmNextWizard = frmTo Screen.MousePointer = iMousePointer Unload frmFrom End SubThe Wizard is started by setting the global form variable to point to the first Wizard form and calling ProcessWizard. When ProcessWizard returns, the Wizard has completed.
ProcessWizard essentially shows the first form modally (after clearing the global variable). This causes execution of ProcessWizard to be suspended until that form is either hidden or unloaded. If that form wants to chain to another form, it sets the global form variable to point to that form and unloads itself, thereby allowing ProcessWizard to continue execution. It finds that the global form variable is not Nothing, so it loops around and does it all over again, continuing to do so until a form unloads itself without specifying a next form in the global variable.
To avoid major surgery to the existing code, we were able to make a simple change to ChainToForm (as shown) to encapsulate this new behaviour. It loads and positions the 'to' form, sets the global variable, then unloads the 'from' form. It is not until the latter actually does unload that ProcessWizard gets to loop the loop one more time to show the next form.
This experience reinforced my conviction that modality (like
focus) in Windows is not something to be played with lightly. Use
modality where you need to, but take care because the beast bites.