A less orthodox architecture: the Create and Review forms swap with each other Here we have another database application, one that's used to keep records of
technical publications. Users can choose an existing entry from the list and edit it using the Review form, or enter a new publication by calling up New. Notice that the New screen has a 'Review' button, and the Review
screen has a 'New' button – this could imply multiple instances of each screen, but let's say that the design calls for screens to be swapped
when these buttons are used. For example, we call up the New screen to enter the details for a new publication, and then we press the Review button to move immediately to the Review screen to enter a review for it. As the New screen loads, it
replaces the Review screen, which is unloaded. As before, the Selector screen is disabled when either the Review screen or the New screen is displayed. There is no elegant way to implement this architecture
using Visual Basic's standard modality features; to do so we would have to somehow defer our request for the Review form to be displayed until the New form was unloaded. We could make it work, but it would be tricky and
it's ugly. It would be much better if we could devise a general mechanism to support the kind of modes we want to enforce. Building a Form Base Class
Forms are really classes in disguise (see Chapter 1), but because Visual Basic doesn't support inheritance at the source code level, we can't build
value-added form classes; the best we can do is to build value-added form instances
by adding custom properties and methods to our forms. We can do this by exploiting the class-like nature of forms and writing a form 'base class' that contains extra properties and methods we'd like to see on every form. This works very well in practice, although it relies on us adding some standard code to every form we create. To see how it works, let's build some methods to save and restore a form's position when it loads and unloads.
The first thing we need to do is define a class, which we'll call CFormAttributes. We'll create a Public instance of this class in every form we create, and this will appear as a property of the form. When we come to
store the form positions with SaveSetting it would be nice to use the form name as a key, but unfortunately there isn't any way for an instance of a Visual Basic class to refer to the object that owns it. This means
we'll need to define the owner as a property in our CFormAttributes class and arrange to set it when we create the instance. Here's the class: Private frmPiSelf As Form
Public Sub SavePosition()
SaveSetting App.Title, "Form Positions", _
frmPiSelf.Name & "-top", frmPiSelf.Top ' ...etc
End Sub
Public Sub RestorePosition() ... End Sub
Public Sub LoadActions(ByVal frmiMe As Form) Set frmPiSelf = frmiMe
RestorePosition frmPiSelf End Sub
Public Sub UnloadActions() SavePosition frmPiSelf End Sub Notice that we've also defined LoadActions and UnloadActions methods.
These make the class more general for when we add to it later. To add our new properties to a form, we need to adopt certain conventions. Firstly we need to define an instance of the class as a form-level variable:
Public My As New CFormAttributes We've called the variable 'My' because it's pretty close to 'Me', and semantically the two are very similar. For example, we can now refer to My.UnloadActions. The only other thing we
need to do is make sure the LoadActions and UnloadActions routines are called: Private Sub Form_Load() My.LoadActions Me End Sub
Private Sub Form_Unload()
My.UnloadActions End Sub Note that we have to pass the owner form reference to LoadActions to initialise our class's Self property.
Towards a General Modality Class We can create and manipulate value-added forms by building a CFormAttributes class and adding our function modality mechanism to it. The central requirement for such a
mechanism is to associate a parent with each form we create. We can do this by adding a Parent property to the CFormAttributes class: Public Parent As Form Now we have somewhere to store a reference to the parent
form, so we need to arrange for it to be set when the form is loaded. Since we can't pass parameters to a form's Show method (or our CFormAttributes instance) we need to do this manually from outside the CFormAttributes
class. We want to be able to do something like this: Public Sub ShowChild Child:=frmReview, Parent:=Me We could make this a global procedure and give it its own .BAS file but we can keep all the code in one place by
making ShowChild a method of the CFormAttributes class. Obviously this means we can't invoke ShowChild to display the first form in a hierarchy, but the only implication of this is that we need to make sure that the
CFormAttributes class recognises that it has no parent when it is destroyed. We can also dispense with the Parent parameter, since we already have a Self reference in the CFormAttributes class. Here's the method, which
we'll actually call NewChild: Public Sub NewChild(ByVal frmiChild As Form) frmiChild.Show Set frmiChild.My.Parent = frmPiSelf frmPiSelf.Enabled = False
End Sub The last statement is the significant one, because this is the one that disables the parent form and creates a new mode. We need a reciprocal action to re-enable the parent when the form unloads, so we
define another method: Public Sub EnableParent() If Not Me.Parent Is Nothing Then Me.Parent.Enabled = True End Sub Unfortunately there's no elegant way to bind this to a form unload; we must
ensure that we call this method from each Form_Unload event: Private Sub Form_Unload() My.EnableParent End Sub (In fact the sample code has a generic UnloadActions method, which takes the
place of EnableParent, but it's clearer if we continue to refer to an EnableParent method.) That takes care of modal child forms, as long as we invoke them with My.NewChild and include the appropriate reciprocal call
in the Form_Unload. We can now build on this to extend the mechanism. To cope with the swapping in our second program, for example, we need to do a couple of extra things: pass on the outgoing form's parent reference to
the new form, and then prevent the parent from being re-enabled when the old form unloads. We can do this by adding a new method and modifying the EnableParent method slightly so the two communicate through a
module-level flag: Private bPiKeepParentDisabled As Boolean
Public Sub SwapMe(ByVal frmiNewChild As Form) frmiNewChild.Show If frmiNewChild.Enabled Then
Set frmiNewChild.My.Parent = Parent bPiKeepParentDisabled = True End If
Unload frmPiSelf End Sub
Public Sub EnableParent() If Not bPiKeepParentDisabled Then
If Not Parent Is Nothing Then Parent.Enabled = True End If End Sub Notice that we check whether the form we're trying to swap to is enabled. If it
isn't, it must already have been loaded, in which case we'll just leave the Parent property alone. This is an ad-hoc test that works in the simple examples we're looking at here, but it may not be general and we'd need
to extend the mechanism to cope with other situations. For example, our mechanism as it stands won't prevent us from trying to swap to a form that's in the middle
of a modal cascade – in fact this would orphan any child forms in the cascade. With a little thought we should be able to extend the mechanism to allow swapping to remove child forms of the form we're trying to swap to, to prevent swapping between forms belonging to other functions in a 'function modal' situation, or to support any other flavours of modality we care to invent.
Extending the FormAttributes Class The beauty of a value-added form class is that it's a simple matter to add new features retrospectively. As an
example let's see how we can add support for pseudo-MDI minimize and restore behaviour. Because all document windows in an MDI application are contained within the Client area of the parent window,
minimizing that window naturally takes all of the children away too. This is convenient, since it instantly clears the application off the desktop (without closing it, of course). Programming with MDI windows in
Visual Basic gives us this minimize behaviour for free, but with an SDI, MTI or DIY-DI application we have no such luxury. Since a Visual Basic form has no Minimize event we must write code that plugs into the Resize
event and decide for ourselves when a form is minimized or restored by investigating the WindowState property. The behaviour we're going to construct will watch for transitions from normal to minimized and from
minimized back to normal (this operation is usually called 'restore'). We'll write the code as a new method of our CFormAttributes class and then we can simply add a call to it from appropriate Resize event handlers.
Trapping the event, of course, is only half the story because then we need to do something to take away the rest of the forms. One possibility is to set the WindowState to follow the window containing the trap, but in
practice that looks messy because Windows animates zoom boxes all over the place and we end up with lots of Task Bar buttons (or icons for earlier versions of Windows NT). It's quicker and visually more effective to
hide all the other forms when we trap a Minimize event, and restore them when we trap a restore event. The only tricky part is to remember the prevailing state of each form before hiding it, just in case any were hidden
already. Here's the code we need: Public PreviouslyVisible As Boolean Private nPiPrevWindowState As Integer
Public Sub PropagateMinMaxEvents () If (frmPiSelf.WindowState = vbMinimized) _
And nPiPrevWindowState = vbNormal Then Call HideAllForms
ElseIf (frmPiSelf.WindowState = vbNormal) _ And nPiPrevWindowState = vbMinimized Then
Call UnhideAllForms End If nPiPrevWindowState = frmPiSelf.WindowState End Sub
Private Sub HideAllForms()
Dim frmForm As Form For Each frmForm In Forms If Not frmForm Is frmPiSelf Then
frmForm.My.PreviouslyVisible = frmForm.Visible frmForm.Visible = False
End If Next frmForm End Sub
Private Sub UnhideAllForms() ' This is just the opposite of HideAllForms End Sub To
activate the new behaviour we need to choose which forms will trigger it and call PropagateMinMaxEvents from their Resize event handlers. Our publication-editing program has this call coded in the Resize events of all
the forms, so minimizing any form hides all the others and shows a single button on the Task Bar. Restoring from that button restores each form to its previous state. To add minimize behaviour to our original example
application we'd code a single call to PropagateMinMaxEvents in the Resize event of the main form (the one carrying the menu bar). This mimics the MDI paradigm more closely because we have a definite 'parent' form.
Since VB5 we can add custom Minimize and Restore events to our forms through the CFormAttributes class. We can do this very simply by making a small modification to our PropagateMinMaxEvents method: Event Minimize()
Event Restore()
Public Sub PropagateMinMaxEvents () If (frmPiSelf.WindowState = vbMinimized) _
And nPiPrevWindowState = vbNormal Then RaiseEvent Minimize
ElseIf (frmPiSelf.WindowState = vbNormal) _ And nPiPrevWindowState = vbMinimized Then
RaiseEvent Restore End If nPiPrevWindowState = frmPiSelf.WindowState End Sub In case you didn't spot it, we've replaced calls
to HideAllForms and UnhideAllForms with calls to RaiseEvent. When we define our CFormAttributes instance on a form, a new object, My, appears in the control list combo box, and when we choose it we see Minimize and
Restore events in the event combo. These work in exactly the same way as normal events, so selecting Minimize inserts an empty procedure called My_Minimize into the code. One caveat is that the syntax for defining our
CFormAttributes instance is slightly different if we want to see the events: Public WithEvents My As CFormAttributes Unfortunately, the New keyword is not allowed in combination with WithEvents, so we also need to
add a line to the Form_Load event: Private Sub Form_Load() Set My = New CFormProperties My.LoadActions Me End Sub |