The types of forms you should be looking to make reusable are what can be considered auxiliary forms. Those that display an application's About information, give spell check functionality, or are logon forms are all likely candidates. More specialized forms that are central to an application's primary function are likely to be too specific to that particular development to make designing them for reuse worthwhile. Alternatively, these specialized forms might still be considered worth making public for use by applications outside those in which they reside.
Writing forms to be reusableAs programmers, we should all be familiar with the idea of reusing forms by now. Windows has had the common File, Print, and Font dialog boxes since its early versions, and these have been available to Visual Basic users through the CommonDialog control right from the first version of Visual Basic. Visual Basic 5 introduced the ability to reuse custom-developed forms in a truly safe way. Visual Basic 4 gave us the ability to declare methods and properties as Public to other forms. Prior to this, only code modules could have access to a form's methods and data. This limitation made form-to-form interaction a little convoluted, with the forms having to interface via a module, and generally made creating a reusable form as a completely encapsulated object impractical.
Visual Basic 5 provided another new capability for forms. Like classes and controls, forms can now raise events, extending our ability to make forms discrete objects. Previously, if we wanted forms to have any two-way interaction, the code within each form had to be aware of the interface of the other. Now we have the ability to create a form that "serves" another form or any other type of module, without any knowledge of its interface, simply by raising events that the client code can deal with as needed. The ability to work in this way is really a prerequisite of reusable components. Without it, a form is always in some way bound, or coupled, to any other code that it works with by its need to have knowledge of that code's interface.
In the following progress report example, you'll find out how to design a generic form that can be reused within many applications. You'll also see how to publicly expose this form to other applications by using a class, allowing its use outside the original application. This topic covers two areas of reuse: reuse of the source code, by which the form is compiled into an application; and reuse of an already compiled form from another application as a distributed object.
Introducing the progress formThe form we'll write here, shown in Figure 14-7, is a generic progress form of the type you often see when installing software or performing some other lengthy process. This type of form serves two basic roles. First, by its presence, the form confirms that the requested process is under way, while giving the user the opportunity to abandon the process if necessary. Second, by constantly displaying the progress of a process, the form makes the process appear faster. With Visual Basic often wrongly accused of being slow, this subjective speed is an important consideration.
Figure 14-7 A generic progress form in action
This example gives us a chance to explore all the different ways you can interact with a form as a component. The form will have properties and methods to enable you to modify its appearance. Additionally, it will raise two events, showing that this ability is not limited to classes.
When designing a form's interface, you must make full use of property procedures to wrap your form's properties. Although you can declare a form's data as Public, by doing so you are exposing it to the harsh world outside your component-a world in which you have no control over the values that might be assigned to that component. A much safer approach is to wrap this data within Property Get and Property Let procedures, giving you a chance both to validate changes prior to processing them and to perform any processing you deem necessary when the property value is changed. If you don't use property procedures, you miss the opportunity to do either of these tasks, and any performance gains you hope for will never appear because Visual Basic creates property procedures for all public data when it compiles your form anyway.
It's also a good policy to wrap the properties of any components or controls that you want to expose in property procedures. This wrapping gives you the same advantages as mentioned previously, plus the ability to change the internal implementation of these properties without affecting your interface. This ability can allow you to change the type of control used. For example, within the example progress form, we use the Windows common ProgressBar control. By exposing properties of the form as property procedures, we would be able to use another control within the form or even draw the progress bar ourselves while maintaining the same external interface through our property procedures. All this prevents any changes to client code, a prerequisite of reusable components.
The generic progress form uses this technique of wrapping properties in property procedures to expose properties of the controls contained within it. Among the properties exposed are the form caption, the progress bar caption, the maximum progress bar value, the current progress bar value, and the visibility of the Cancel command button. Although all of these properties can be reached directly, by exposing them through property procedures, we're able to both validate new settings and perform other processing if necessary. This is illustrated by the AllowCancel and ProgressBarValue properties. The AllowCancel property controls not only the Visible state of the Cancel command button but also the height of the form, as shown in this code segment:
Public Property Let AllowCancel (ByVal ibNewValue As Boolean) If ibNewValue = True Then cmdCancel.Visible = True Me.Height = 2460 Else cmdCancel.Visible = False Me.Height = 1905 End If Me.Refresh End Property
The ProgressBarValue property validates a new value, avoiding an unwanted error that might occur if the value is set greater than the current maximum:
Public Property Let ProgressBarValue(ByVal ilNewValue As Long) ' Ensure that the new progress bar value is not ' greater than the maximum value. If Abs(ilNewValue) > Abs(gauProgress.Max) Then ilNewValue = gauProgress.Max End If gauProgress.Value = ilNewValue Me.Refresh End Property
The progress form eventsThe progress form can raise two events. Events are most commonly associated with controls, but can be put to equally good use within other components. To have our form generate events, we must declare each event within the general declarations for the form as shown here:
Public Event PerformProcess(ByRef ProcessData As Variant) Public Event QueryAbandon(ByRef Ignore As Boolean)
Progress forms are usually displayed modally. Essentially, they give the user something to look at while the application is too busy to respond. Because of this we have to have some way for our progress form appear modal, while still allowing the application's code to execute. We do this by raising the PerformProcess event once the form has finished loading. This event will be executed within the client code, where we want our process to be carried out.
Private Sub Form_Activate() Static stbActivated As Boolean ' (Re)Paint this form. Me.Refresh If Not stbActivated Then stbActivated = True ' Now this form is visible, call back into the calling ' code so that it may perform whatever action it wants. RaiseEvent PerformProcess(m_vProcessData) ' Now that the action is complete, unload me. Unload Me End If End Sub
Components used in this way are said to perform a callback. In this case we show the form, having previously prepared code in the PerformProcess event handler for it to callback and execute once it has finished loading. This allows us to neatly sidestep the fact that when we display a form modally, the form now has the focus and no further code outside it is executed until it unloads.
The final piece of sample code that we need to look at within our progress form is the code that generates the QueryAbandon event. This event allows the client code to obtain user confirmation before abandoning what it's doing. This event is then triggered when the Cancel command button is clicked. By passing the Ignore Boolean value by reference, we give the event handling routine in the client the opportunity to change this value in order to work in the same way as the Cancel value within a form's QueryUnload event. When we set Ignore to True, the event handling code can prevent the process from being abandoned. When we leave Cancel as False, the progress form will continue to unload. The QueryAbandon event is raised as follows:
Private Sub cmdCancel_Click() Dim bCancel As Boolean bCancel = False RaiseEvent QueryAbandon(bCancel) If bCancel = False Then Unload Me End Sub
From this code, you can see how the argument of the QueryAbandon event controls whether or not the form is unloaded, depending on its value after the event has completed.
Using the progress formThe code that follows illustrates how the progress form can be employed. First we have to create an instance of the form. This must be placed in the client module's Declarations section because it will be raising events within this module, much the same way as controls do. Forms and classes that raise events are declared as WithEvents, in the following way:
Private WithEvents frmPiProg As frmProgress
We must declare the form in this way; otherwise, we wouldn't have access to the form's events. By using this code, the form and its events will appear within the Object and Procedure combo boxes in the Code window, just as for a control.
Now that the form has been declared, we can make use of it during our lengthy process. First we must create a new instance of it, remembering that the form does not exist until it has actually been Set with the New keyword. When this is done we can set the form's initial properties and display it, as illustrated here:
' Instantiate the progress form. Set frmPiProg = New frmProgress ' Set up the form's initial properties. frmPiProg.FormCaption = "File Search" frmPiProg.ProgressBarMax = 100 frmPiProg.ProgressBarValue = 0 frmPiProg.ProgressCaption = _ "Searching for file. Please wait..." ' Now Display it modally. frmPiProg.Show vbModal, Me
Now that the progress form is displayed, it will raise the PerformAction event in our client code, within which we can carry out our lengthy process. This allows the progress form to be shown modally, but still allow execution within the client code.
Private Sub frmPiProg_PerformProcess(ProcessData As Variant) Dim nPercentComplete As Integer mbProcessCancelled = False Do ' Update the form's progress bar. nPercentComplete = nPercentComplete + 1 frmPiProg.ProgressBarValue = nPercentComplete ' Peform your action. ' You must include DoEvents in your process or any ' clicks on the Cancel button will not be responded to. DoEvents Loop While mbProcessCancelled <> True _ And nPercentComplete < frmPiProg.ProgressBarMax End Sub
The final piece of code we need to put into our client is the event handler for the QueryAbandon event that the progress form raises when the user clicks the Cancel button. This event gives us the chance to confirm or cancel the abandonment of the current process, generally after seeking confirmation from the user. An example of how this might be done follows:
Private Sub frmPiProg_QueryAbandon(Ignore As Boolean) If MsgBox("Are you sure you want to cancel?", _ vbQuestion Or vbYesNo, Me.Caption) = vbNo Then Ignore = True mbProcessCancelled = True End If End Sub
From this example, you can see that in order to use the progress form, the parent code simply has to set the form's properties, display it, and deal with any events it raises.
Making a form publicAlthough forms do not have an Instancing property and cannot be made public outside their application, you can achieve this effect by using a class module as an intermediary. By mirroring the events, methods, and properties of your form within a class with an Instancing property other than Private, making sure that the project type is ActiveX EXE or ActiveX DLL, you can achieve the same results as you can by making a form public.
Using the progress form as an example, we will create a public class named CProgressForm. This class will have all the properties and methods of the progress form created earlier. Where a property of the class is accessed, the class will merely delegate the implementation of that property to the underlying form, making it public. Figure 14-8 shows this relationship, with the client application having access to the CProgressForm class but not frmProgress, but the CProgressForm class having an instance of frmProgress privately. To illustrate these relationships, we will show how the ProgressBarValue property is made public.
First we need to declare a private instance of the form within the Declarations section of our class:
Private WithEvents frmPiProgressForm As frmProgress
Figure 14-8 Making a form public using a public class as an intermediary
Here we see how the ProgressBarValue property is made public by using the class as an intermediary:
Public Property Let ProgressBarValue(ByVal ilNewValue As Long) frmPiProgressForm.ProgressBarValue = ilNewValue End Property Public Property Get ProgressBarValue() As Long ProgressBarValue = frmPiProgressForm.ProgressBarValue End Property
Similarly, we can subclass the PerformProcess and QueryAbandon events, allowing us to make public the full functionality of the progress form. For example, we could subclass the QueryAbandon event by reraising it from the class, in reaction to the initial event raised by the form, and passing by reference the initial Ignore argument within the new event. This way the client code can still modify the Ignore argument of the original form's event.
Private Sub frmPiProgressForm_QueryAbandon(Ignore As Boolean) RaiseEvent QueryAbandon(Ignore) End Sub
There is a difficulty with exposing the progress form in this way. The form has a Show method that we must add to the class. Because we're using the form within another separate application, this method cannot display the form modally to the client code. One solution is to change the Show method of the CProgressForm class so that it always displays the progress form modelessly.
Another possible solution is to use a control instead of a public class to expose the form to the outside world. Those of you who have used the common dialogs before will be familiar with this technique. This enables you to make the form public in the same way as with CProgressClass, but additionally you can add a Display method, in which you call the form's Show method, showing it modally to the form that the control is hosted on.
Public Sub Display(ByVal inCmdShow As Integer) ' Display the progress form. frmPiProgressForm.Show inCmdShow, UserControl.Parent End Sub